From 2384f75d37fef5eccc355a7930f193ffe6b5511d Mon Sep 17 00:00:00 2001 From: Ben Rabinovich Date: Mon, 11 Aug 2025 23:48:31 +0300 Subject: [PATCH 1/2] feat: ci pipeline --- .github/workflows/ci.yml | 33 ++++++++++ .github/workflows/release.yml | 70 ++++++++++++++++++++ README.md | 27 +++++--- examples/basic.ts | 79 ----------------------- examples/static-all.ts | 116 ---------------------------------- examples/static-startup.ts | 87 ------------------------- package.json | 9 +-- 7 files changed, 123 insertions(+), 298 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml delete mode 100644 examples/basic.ts delete mode 100644 examples/static-all.ts delete mode 100644 examples/static-startup.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8196a63 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +jobs: + build-test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + registry-url: "https://registry.npmjs.org" + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Run unit tests + run: npm run test:run + + - name: Build + run: npm run build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b3c4d79 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,70 @@ +name: Release + +on: + push: + tags: + - "v*.*.*" + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Ensure tag is on main + run: | + DEFAULT_BRANCH=main + TAG_REF="${GITHUB_REF#refs/tags/}" + TAG_SHA=$(git rev-list -n 1 "$TAG_REF") + git fetch origin "$DEFAULT_BRANCH":"refs/remotes/origin/$DEFAULT_BRANCH" --depth=1 + MAIN_SHA=$(git rev-parse "origin/$DEFAULT_BRANCH") + if [ "$TAG_SHA" != "$MAIN_SHA" ]; then + echo "Tag $TAG_REF must point to latest $DEFAULT_BRANCH ($MAIN_SHA), but points to $TAG_SHA" >&2 + exit 1 + fi + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20.x" + cache: "npm" + registry-url: "https://registry.npmjs.org" + + - name: Install dependencies + run: npm ci + + - name: Verify version matches tag and not private + id: verify + run: | + TAG_VERSION=${GITHUB_REF#refs/tags/v} + PKG_VERSION=$(node -p "require('./package.json').version") + IS_PRIVATE=$(node -p "Boolean(require('./package.json').private).toString()") + if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then + echo "Tag version v$TAG_VERSION does not match package.json version $PKG_VERSION" >&2 + exit 1 + fi + if [ "$IS_PRIVATE" = "true" ]; then + echo "package.json has private:true. Publishing public packages requires private:false" >&2 + exit 1 + fi + echo "version=$PKG_VERSION" >> $GITHUB_OUTPUT + + - name: Typecheck + run: npm run typecheck + + - name: Test + run: npm run test:run + + - name: Build + run: npm run build + + - name: Publish to npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + npm publish --access public --verbose diff --git a/README.md b/README.md index 1bb104f..b5b88fc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,16 @@ # Toolception – Dynamic MCP Tooling Library +## Table of Contents + +- [Quickstart](#quickstart) +- [Static startup](#static-startup) +- [API](#api) +- [Client ID lifecycle](#client-id-lifecycle) +- [Session ID lifecycle](#session-id-lifecycle) +- [Tool types](#tool-types) +- [Startup modes](#startup-modes) +- [License](#license) + ## Quickstart Install: @@ -91,8 +102,6 @@ createMcpServer({ }); ``` -See `examples/` for runnable snippets. See `AGENTS.md` for LLM/agent-oriented guidance. - ## API - createMcpServer(options): creates an MCP server with dynamic/static tool management and Fastify transport. @@ -103,10 +112,13 @@ See `examples/` for runnable snippets. See `AGENTS.md` for LLM/agent-oriented gu - exposurePolicy?: ExposurePolicy - context?: unknown (passed to loaders when resolving tools) - http?: { host?: string; port?: number; basePath?: string; cors?: boolean; logger?: boolean } + - mcp?: { name?: string; version?: string; capabilities?: Record } + - configSchema?: object (served at `GET /.well-known/mcp-config`) -Meta-tools (enabled by default in DYNAMIC): +Meta-tools (enabled by default when mode is DYNAMIC): -- enable_toolset, disable_toolset, list_toolsets, list_tools +- enable_toolset, disable_toolset, list_tools (registered whenever meta-tools are enabled) +- list_toolsets, describe_toolset (registered only in DYNAMIC mode) ## Client ID lifecycle @@ -203,9 +215,6 @@ The server operates in one of two primary modes (legacy load-all is not recommen - Meta-tools limited to `list_tools` by default - Best for known, consistent tool requirements -## Examples +## License -- See `examples/` for runnable scripts: - - `npx --yes tsx examples/basic.ts` - - `npx --yes tsx examples/static-startup.ts` - - `npx --yes tsx examples/static-all.ts` +Apache-2.0. See `LICENSE` for details. diff --git a/examples/basic.ts b/examples/basic.ts deleted file mode 100644 index 2a184fa..0000000 --- a/examples/basic.ts +++ /dev/null @@ -1,79 +0,0 @@ -// Run with: npx --yes tsx examples/basic.ts -import { createMcpServer } from "../src/server/createMcpServer.js"; -import type { ToolSetCatalog, ModuleLoader } from "../src/types/index.js"; - -// Catalog: one direct toolset and one module-derived toolset -const catalog: ToolSetCatalog = { - core: { - name: "Core", - description: "Core utilities", - tools: [ - { - name: "ping", - description: "Responds with pong", - inputSchema: { type: "object", properties: {} }, - handler: async () => ({ content: [{ type: "text", text: "pong" }] }), - }, - ], - }, - ext: { name: "Extensions", description: "Extra tools", modules: ["ext"] }, -}; - -const moduleLoaders: Record = { - ext: async () => [ - { - name: "echo", - description: "Echo back provided text", - inputSchema: { - type: "object", - properties: { text: { type: "string" } }, - required: ["text"], - }, - handler: async ({ text }: { text: string }) => ({ - content: [{ type: "text", text }], - }), - }, - ], -}; - -const { start, close } = await createMcpServer({ - catalog, - moduleLoaders, - startup: { mode: "DYNAMIC" }, - http: { port: 3000 }, - mcp: { name: "toolception-example", version: "0.1.0" }, - configSchema: { - $schema: "https://json-schema.org/draft/2020-12/schema", - type: "object", - properties: { - FMP_ACCESS_TOKEN: { - type: "string", - title: "FMP Access Token", - description: "Financial Modeling Prep API access token", - }, - FMP_TOOL_SETS: { - type: "string", - title: "Tool Sets (Optional)", - description: - "Comma-separated list of tool sets to load (e.g., 'search,company,quotes'). If not specified, all tools will be loaded.", - }, - DYNAMIC_TOOL_DISCOVERY: { - type: "string", - title: "Dynamic Tool Discovery (Optional)", - description: - "Enable dynamic toolset management. Set to 'true' to use meta-tools.", - }, - }, - required: [], - }, -}); - -await start(); -console.log("Server started on http://localhost:3000"); - -const shutdown = async () => { - await close(); - process.exit(0); -}; -process.on("SIGINT", shutdown); -process.on("SIGTERM", shutdown); diff --git a/examples/static-all.ts b/examples/static-all.ts deleted file mode 100644 index 7bf70c1..0000000 --- a/examples/static-all.ts +++ /dev/null @@ -1,116 +0,0 @@ -// Run with: npx --yes tsx examples/static-all.ts -import { createMcpServer } from "../src/server/createMcpServer.js"; -import type { ToolSetCatalog, ModuleLoader } from "../src/types/index.js"; - -const catalog: ToolSetCatalog = { - search: { name: "Search", description: "Search tools", modules: ["search"] }, - company: { - name: "Company", - description: "Company tools", - modules: ["company"], - }, - quotes: { name: "Quotes", description: "Market quotes", modules: ["quotes"] }, - news: { name: "News", description: "News tools", modules: ["news"] }, -}; - -const moduleLoaders: Record = { - search: async () => [ - { - name: "find", - description: "find", - inputSchema: { - type: "object", - properties: { q: { type: "string" } }, - required: ["q"], - }, - handler: async ({ q }: { q: string }) => ({ - content: [{ type: "text", text: `q=${q}` }], - }), - }, - ], - company: async () => [ - { - name: "profile", - description: "profile", - inputSchema: { - type: "object", - properties: { symbol: { type: "string" } }, - required: ["symbol"], - }, - handler: async ({ symbol }: { symbol: string }) => ({ - content: [{ type: "text", text: `${symbol}` }], - }), - }, - ], - quotes: async () => [ - { - name: "price", - description: "price", - inputSchema: { - type: "object", - properties: { symbol: { type: "string" } }, - required: ["symbol"], - }, - handler: async ({ symbol }: { symbol: string }) => ({ - content: [{ type: "text", text: `${symbol}: 1.23` }], - }), - }, - ], - news: async () => [ - { - name: "latest", - description: "latest", - inputSchema: { - type: "object", - properties: { topic: { type: "string" } }, - required: ["topic"], - }, - handler: async ({ topic }: { topic: string }) => ({ - content: [{ type: "text", text: `${topic}` }], - }), - }, - ], -}; - -const { start, close } = await createMcpServer({ - catalog, - moduleLoaders, - startup: { mode: "STATIC", toolsets: "ALL" }, - registerMetaTools: false, - http: { port: 3002 }, - mcp: { name: "toolception-static-all", version: "0.1.0" }, - configSchema: { - $schema: "https://json-schema.org/draft/2020-12/schema", - type: "object", - properties: { - FMP_ACCESS_TOKEN: { - type: "string", - title: "FMP Access Token", - description: "Financial Modeling Prep API access token", - }, - FMP_TOOL_SETS: { - type: "string", - title: "Tool Sets (Optional)", - description: - "Comma-separated list of tool sets to load (e.g., 'search,company,quotes'). If not specified, all tools will be loaded.", - }, - DYNAMIC_TOOL_DISCOVERY: { - type: "string", - title: "Dynamic Tool Discovery (Optional)", - description: - "Enable dynamic toolset management. Set to 'true' to use meta-tools.", - }, - }, - required: [], - }, -}); - -await start(); -console.log("Server started on http://localhost:3002"); - -const shutdown = async () => { - await close(); - process.exit(0); -}; -process.on("SIGINT", shutdown); -process.on("SIGTERM", shutdown); diff --git a/examples/static-startup.ts b/examples/static-startup.ts deleted file mode 100644 index b13a1f3..0000000 --- a/examples/static-startup.ts +++ /dev/null @@ -1,87 +0,0 @@ -// Run with: npx --yes tsx examples/static-startup.ts -import { createMcpServer } from "../src/server/createMcpServer.js"; -import type { ToolSetCatalog, ModuleLoader } from "../src/types/index.js"; - -const catalog: ToolSetCatalog = { - search: { name: "Search", description: "Search tools", modules: ["search"] }, - company: { - name: "Company", - description: "Company tools", - modules: ["company"], - }, - quotes: { name: "Quotes", description: "Market quotes", modules: ["quotes"] }, -}; - -const moduleLoaders: Record = { - search: async () => [ - { - name: "find", - description: "Simple search placeholder", - inputSchema: { - type: "object", - properties: { q: { type: "string" } }, - required: ["q"], - }, - handler: async ({ q }: { q: string }) => ({ - content: [{ type: "text", text: `query=${q}` }], - }), - }, - ], - quotes: async () => [ - { - name: "price", - description: "Return fake price", - inputSchema: { - type: "object", - properties: { symbol: { type: "string" } }, - required: ["symbol"], - }, - handler: async ({ symbol }: { symbol: string }) => ({ - content: [{ type: "text", text: `${symbol}: 123.45` }], - }), - }, - ], -}; - -const { start, close } = await createMcpServer({ - catalog, - moduleLoaders, - startup: { mode: "STATIC", toolsets: ["search", "quotes"] }, - registerMetaTools: false, - http: { port: 3001 }, - mcp: { name: "toolception-static", version: "0.1.0" }, - configSchema: { - $schema: "https://json-schema.org/draft/2020-12/schema", - type: "object", - properties: { - FMP_ACCESS_TOKEN: { - type: "string", - title: "FMP Access Token", - description: "Financial Modeling Prep API access token", - }, - FMP_TOOL_SETS: { - type: "string", - title: "Tool Sets (Optional)", - description: - "Comma-separated list of tool sets to load (e.g., 'search,company,quotes'). If not specified, all tools will be loaded.", - }, - DYNAMIC_TOOL_DISCOVERY: { - type: "string", - title: "Dynamic Tool Discovery (Optional)", - description: - "Enable dynamic toolset management. Set to 'true' to use meta-tools.", - }, - }, - required: [], - }, -}); - -await start(); -console.log("Server started on http://localhost:3001"); - -const shutdown = async () => { - await close(); - process.exit(0); -}; -process.on("SIGINT", shutdown); -process.on("SIGTERM", shutdown); diff --git a/package.json b/package.json index 4cfdffd..ab5250f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "toolception", "version": "0.1.0", - "private": true, + "private": false, "type": "module", "main": "dist/index.js", "module": "dist/index.js", @@ -18,12 +18,7 @@ "test": "vitest", "test:run": "vitest run", "test:coverage": "vitest run --coverage", - "prepublishOnly": "npm run typecheck && npm run build && npm run test:run", - "dev:basic": "npx --yes tsx examples/basic.ts", - "dev:static-startup": "npx --yes tsx examples/static-startup.ts", - "dev:static-all": "npx --yes tsx examples/static-all.ts", - "dev:server-demo": "npx --yes tsx tests/smoke-e2e/server-demo.ts", - "dev:client-demo": "npx --yes tsx tests/smoke-e2e/client-demo.ts" + "prepublishOnly": "npm run typecheck && npm run build && npm run test:run" }, "peerDependencies": {}, "dependencies": { From 7ae9b295693379747a3b9644662de9289fc73fd7 Mon Sep 17 00:00:00 2001 From: Ben Rabinovich Date: Tue, 12 Aug 2025 11:04:31 +0300 Subject: [PATCH 2/2] chore: update ci and docs --- .github/workflows/release.yml | 11 +-- README.md | 122 +++++++++++++++++++++++++++------- 2 files changed, 104 insertions(+), 29 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b3c4d79..89ab1f3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,13 +16,16 @@ jobs: with: fetch-depth: 0 - - name: Ensure tag is on main + - name: Ensure tag is on default branch run: | - DEFAULT_BRANCH=main + DEFAULT_BRANCH="${{ github.event.repository.default_branch }}" + if [ -z "$DEFAULT_BRANCH" ]; then + DEFAULT_BRANCH=$(git remote show origin | sed -n '/HEAD branch/s/.*: //p') + fi TAG_REF="${GITHUB_REF#refs/tags/}" TAG_SHA=$(git rev-list -n 1 "$TAG_REF") - git fetch origin "$DEFAULT_BRANCH":"refs/remotes/origin/$DEFAULT_BRANCH" --depth=1 - MAIN_SHA=$(git rev-parse "origin/$DEFAULT_BRANCH") + git fetch origin "$DEFAULT_BRANCH":"refs/remotes/origin/$DEFAULT_BRANCH" + MAIN_SHA=$(git rev-parse "refs/remotes/origin/$DEFAULT_BRANCH") if [ "$TAG_SHA" != "$MAIN_SHA" ]; then echo "Tag $TAG_REF must point to latest $DEFAULT_BRANCH ($MAIN_SHA), but points to $TAG_SHA" >&2 exit 1 diff --git a/README.md b/README.md index b5b88fc..5e1f4af 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ # Toolception – Dynamic MCP Tooling Library +[![npm version](https://img.shields.io/npm/v/toolception.svg)](https://www.npmjs.com/package/toolception) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) + ## Table of Contents -- [Quickstart](#quickstart) +- [Starter guide](#starter-guide) - [Static startup](#static-startup) - [API](#api) - [Client ID lifecycle](#client-id-lifecycle) @@ -11,24 +14,31 @@ - [Startup modes](#startup-modes) - [License](#license) -## Quickstart +## Starter guide -Install: +### Step 1: Install ```bash npm i toolception ``` -Create a server with dynamic tool management (Fastify transport included): +### Step 2: Import Toolception ```ts import { createMcpServer } from "toolception"; +``` +### Step 3: Define a toolset catalog + +```ts const catalog = { quotes: { name: "Quotes", description: "Market quotes", modules: ["quotes"] }, }; +``` + +### Step 4: Define a tool -// A simple MCP server tool used below +```ts const quoteTool = { name: "price", description: "Return a fake price", @@ -41,15 +51,19 @@ const quoteTool = { content: [{ type: "text", text: `${symbol}: 123.45` }], }), } as const; +``` + +### Step 5: Provide module loaders -// Module loaders return MCP server tools (McpToolDefinition[]) that will be -// registered on the MCP server. Each tool must include `name`, `description`, -// a JSON Schema `inputSchema`, and a `handler` that returns MCP content -// (e.g., { content: [{ type: 'text', text: '...' }] }). +```ts const moduleLoaders = { quotes: async () => [quoteTool], }; +``` + +### Step 6: (Optional) Configuration schema +```ts const configSchema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", @@ -59,16 +73,24 @@ const configSchema = { }, required: ["REQUIRED_PARAM"], } as const; +``` + +### Step 7: Create and start the MCP server +```ts const { start, close } = await createMcpServer({ catalog, moduleLoaders, startup: { mode: "DYNAMIC" }, http: { port: 3000 }, + // configSchema, // uncomment to expose at /.well-known/mcp-config }); await start(); +``` + +### Step 8: Graceful shutdown -// Graceful shutdown +```ts process.on("SIGINT", async () => { await close(); process.exit(0); @@ -104,21 +126,71 @@ createMcpServer({ ## API -- createMcpServer(options): creates an MCP server with dynamic/static tool management and Fastify transport. - - catalog: Record - - moduleLoaders?: Record (loader returns McpToolDefinition[]; used when enabling toolsets that reference its key) - - startup?: { mode: "DYNAMIC" | "STATIC"; toolsets?: string[] | "ALL" } - - registerMetaTools?: boolean - - exposurePolicy?: ExposurePolicy - - context?: unknown (passed to loaders when resolving tools) - - http?: { host?: string; port?: number; basePath?: string; cors?: boolean; logger?: boolean } - - mcp?: { name?: string; version?: string; capabilities?: Record } - - configSchema?: object (served at `GET /.well-known/mcp-config`) - -Meta-tools (enabled by default when mode is DYNAMIC): - -- enable_toolset, disable_toolset, list_tools (registered whenever meta-tools are enabled) -- list_toolsets, describe_toolset (registered only in DYNAMIC mode) +### createMcpServer(options) + +Creates an MCP server with dynamic/static tool management and Fastify HTTP transport. + +#### options.catalog (required) + +`Record` + +- Defines available toolsets to expose. Each item includes `name`, `description`, optional inline `tools`, optional `modules` (for lazy loaders), and optional `decisionCriteria`. + +#### options.moduleLoaders (optional) + +`Record` + +- Maps module keys to async loaders returning `McpToolDefinition[]`. Referenced by toolsets via `modules: [key]`. + +#### options.startup (optional) + +`{ mode?: "DYNAMIC" | "STATIC"; toolsets?: string[] | "ALL" }` + +- Controls startup behavior. In STATIC mode, pre-load specific toolsets (or ALL). In DYNAMIC, register meta-tools and load on demand. + +#### options.registerMetaTools (optional) + +`boolean` (default: true in DYNAMIC mode; false in STATIC unless explicitly set) + +- Whether to register management tools like `enable_toolset`, `disable_toolset`, `list_tools`. + +#### options.exposurePolicy (optional) + +`ExposurePolicy` + +- Limits and namespacing for registered tools (e.g., `maxActiveToolsets`, `namespaceToolsWithSetKey`, `allowlist`/`denylist`). + +#### options.context (optional) + +`unknown` + +- Arbitrary context passed to `moduleLoaders` during tool resolution. + +#### options.http (optional) + +`{ host?: string; port?: number; basePath?: string; cors?: boolean; logger?: boolean }` + +- Fastify transport configuration. Defaults: host `0.0.0.0`, port `3000`, basePath `/`, CORS enabled, logger disabled. + +#### options.mcp (optional) + +`{ name?: string; version?: string; capabilities?: Record }` + +- Overrides MCP server identity and capabilities; `tools.listChanged` is set automatically based on mode. + +#### options.configSchema (optional) + +`object` + +- JSON Schema exposed at `GET /.well-known/mcp-config` for client discovery. + +### Meta-tools + +Enabled by default when mode is DYNAMIC (or when `registerMetaTools` is true): + +- `enable_toolset`, `disable_toolset`, `list_tools` + Only in DYNAMIC mode: +- `list_toolsets`, `describe_toolset` ## Client ID lifecycle