diff --git a/bun.lock b/bun.lock index 733e67e5..9a47f208 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,8 @@ "": { "name": "g", "dependencies": { + "@cfworker/json-schema": "^4.1.1", + "@modelcontextprotocol/server": "^2.0.0-beta.4", "@openfeature/core": "1.9.2", "@openfeature/flagd-provider": "^0.13.4", "@openfeature/server-sdk": "1.20.2", @@ -23,7 +25,7 @@ "pino-loki": "^2.3.0", "pino-opentelemetry-transport": "^3.0.0", "posthog-node": "^5.26.2", - "zod": "^4.3.6", + "zod": "^4.4.3", }, "devDependencies": { "@eslint/js": "^10.0.1", @@ -159,6 +161,8 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@cfworker/json-schema": ["@cfworker/json-schema@4.1.1", "", {}, "sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], @@ -249,6 +253,10 @@ "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.0.0-beta.4", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-nsMXd4wQBKzmph6r+WOhum+mXjDYljTAqwY/XUg3hLtvNOQ8+JVqBSJOVCMJvx9lXhTpTOPrGZ3BuNiaNPjSvg=="], + + "@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.0.0-beta.4", "", { "dependencies": { "@modelcontextprotocol/core": "2.0.0-beta.4", "zod": "^4.2.0" } }, "sha512-pjMZcNEt1dOq0aJCcY3b7w1Ayh+qmQN2xlfTELcGV9yiAjEGIczBPBLWWW0k0zzo5T8kiv15jtKPsWeHGxLvLg=="], + "@openfeature/core": ["@openfeature/core@1.9.2", "", {}, "sha512-0lX0xYTflLrjiYNlareYmdV98xEddR5+PhcuoGvH+BMIqpZ2icAC7us9Uv86KRVqofXvpAUwpP32wgqmtUFs8Q=="], "@openfeature/flagd-core": ["@openfeature/flagd-core@1.2.0", "", { "dependencies": { "ajv": "^8.12.0", "imurmurhash": "^0.1.4", "json-logic-engine": "^4.0.2", "object-hash": "^3.0.0", "semver": "^7.6.3" }, "peerDependencies": { "@openfeature/core": ">=1.6.0" } }, "sha512-b44KqS5A9CdlQEa3FVH2SLtZ0GNMOp2mSHSpGM8dkbEGMj2nnRX/sJD/TTjBbyOyHlOPmbtjDHtKTE7crjthlA=="], @@ -873,7 +881,7 @@ "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], - "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], diff --git a/package.json b/package.json index 0206ada2..588abfe4 100644 --- a/package.json +++ b/package.json @@ -26,19 +26,17 @@ "sup": "AGENT_TYPE=support bun --env-file=.auth apps/cli/chat.ts", "tui": "TUI_MODE=1 bun --env-file=.auth apps/tui/index.tsx", "tui:debug": "AGENT_TYPE=coding bun --env-file=.auth apps/tui/index.tsx", - "build": "bun run type-check && vite build", "build:ci": "bun run build && bun run test:ci", "build:dev": "vite build --mode development", "prebuild": "bun scripts/generate-tool-registry.ts", "preview": "vite preview", - "lint": "eslint . --cache --max-warnings 0", "lint:fix": "eslint . --cache --fix --max-warnings 0", "test:bail": "bun test --coverage --bail ./packages/**/*.test.ts", "test:cov": "bun test --coverage ./src/**/*.test.ts ./tests/**/*.test.ts", "test:e2e": "bun test ./tests/e2e/e2e.test.ts", - "test:report": "bun test ./tests/unit/*.test.ts --reporter=junit --no-cache --reporter-outfile", // pass [filepath] + "test:report": "bun test ./tests/unit/*.test.ts --reporter=junit --no-cache --reporter-outfile", "test:watch": "bun test --watch", "tests": "bun test ./tests/**/*.test.ts", "tests:int": "bun test ./tests/integration/*.test.ts", @@ -49,8 +47,7 @@ "clean:fr": "find . -name 'dist' -type d -prune -exec rm -rf {} + && find . -name '*.tsbuildinfo' -delete && tsc --build --clean", "clean:nuke": "find . -type f \\( -name '*.js' -o -name '*.d.ts' -o -name '*.d.ts.map' -o -name '*.js.map' -o -name '*.tsbuildinfo' \\) -not -path '*/node_modules/*' -delete && find . -name 'dist' -type d -prune -exec rm -rf {} +", "reinstall": "bun pm cache rm && rm -rf node_modules && bun install", - "rebuild": "bun run clean:dist && bun run clean:tsbuildinfo && bun tsc --build --force", - + "rebuild": "bun run clean:dist && bun run clean:tsbuildinfo && bun tsc --build --force", "tui:coda": "TUI_MODE=1 AGENT=coding bun --env-file=.auth apps/tui/index.tsx" }, "keywords": [], @@ -58,6 +55,8 @@ "license": "ISC", "description": "", "dependencies": { + "@cfworker/json-schema": "^4.1.1", + "@modelcontextprotocol/server": "^2.0.0-beta.4", "@openfeature/core": "1.9.2", "@openfeature/flagd-provider": "^0.13.4", "@openfeature/server-sdk": "1.20.2", @@ -76,7 +75,7 @@ "pino-loki": "^2.3.0", "pino-opentelemetry-transport": "^3.0.0", "posthog-node": "^5.26.2", - "zod": "^4.3.6" + "zod": "^4.4.3" }, "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/packages/tools/day-tool/day-server.js b/packages/tools/day-tool/day-server.js new file mode 100644 index 00000000..293e3a24 --- /dev/null +++ b/packages/tools/day-tool/day-server.js @@ -0,0 +1,36 @@ +// day-server.mjs +import { WebSocket, WebSocketServer } from 'ws'; + +const wss = new WebSocketServer({ port: 9001 }); + +console.log('Day MCP Server running on ws://localhost:9001'); + +wss.on('connection', (ws) => { + ws.on('message', (data) => { + const msg = JSON.parse(data); + + if (msg.method === 'tools/list') { + ws.send(JSON.stringify({ + jsonrpc: '2.0', + id: msg.id, + result: { + tools: [{ + name: 'get_day', + description: 'Returns current day of the week' + }] + } + })); + } + + if (msg.method === 'tools/call' && msg.params.name === 'get_day') { + const day = new Date().toLocaleDateString('en-US', { weekday: 'long' }); + ws.send(JSON.stringify({ + jsonrpc: '2.0', + id: msg.id, + result: { + content: [{ type: 'text', text: `Today is ${day}` }] + } + })); + } + }); +}); \ No newline at end of file diff --git a/packages/tools/day-tool/index.ts b/packages/tools/day-tool/index.ts new file mode 100644 index 00000000..d882997e --- /dev/null +++ b/packages/tools/day-tool/index.ts @@ -0,0 +1,22 @@ +// packages/tools/day/index.ts +// +// Drop-in MCP v2 tool. Replaces the standalone day-server.mjs, which ran +// its own ws://localhost:9001 JSON-RPC server. Under v2, tools are plain +// functions loaded into the shared mcpServer.ts process — there is no +// per-tool transport, socket, or server instance anymore. +// +// This file is the module's barrel: it's the single import surface for +// the tool (run, inputSchema, and the input type all re-export from here). + +import type { DayGetInput } from './schema'; + +export { inputSchema } from './schema'; +export type { DayGetInput } from './schema'; + +export async function run(_params: DayGetInput): Promise<{ day: string; text: string }> { + const day = new Date().toLocaleDateString('en-US', { weekday: 'long' }); + return { + day, + text: `Today is ${day}` + }; +} \ No newline at end of file diff --git a/packages/tools/day-tool/manifest.json b/packages/tools/day-tool/manifest.json new file mode 100644 index 00000000..ad5e9bbd --- /dev/null +++ b/packages/tools/day-tool/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "day.get", + "description": "Returns the current day of the week", + "importPath": "day/index.ts", + "inputSchema": { + "type": "object", + "properties": {}, + "additionalProperties": false + } +} \ No newline at end of file diff --git a/packages/tools/day-tool/schema.ts b/packages/tools/day-tool/schema.ts new file mode 100644 index 00000000..98e9c1f8 --- /dev/null +++ b/packages/tools/day-tool/schema.ts @@ -0,0 +1,15 @@ +// packages/tools/day/schema.ts +// +// JSON Schema (2020-12) for the day.get tool's input. This tool takes no +// arguments today, but the schema is kept explicit rather than omitted so +// fromJsonSchema() in mcpServer.ts has a real object to validate against, +// and so adding an argument later (e.g. `timezone`) is a schema change, +// not a new file. + +export const inputSchema = { + type: 'object', + properties: {}, + additionalProperties: false +} as const; + +export type DayGetInput = Record; \ No newline at end of file diff --git a/packages/tools/day-tool/test.ts b/packages/tools/day-tool/test.ts new file mode 100644 index 00000000..08c944f6 --- /dev/null +++ b/packages/tools/day-tool/test.ts @@ -0,0 +1,78 @@ +// packages/tools/day/test.ts +// +// Run with: bun test packages/tools/day/test.ts + +import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { run } from './index'; +import { inputSchema } from './schema'; + +describe('day.get', () => { + it('returns a weekday name matching the current system date', async () => { + const expected = new Date().toLocaleDateString('en-US', { weekday: 'long' }); + const result = await run({}); + expect(result.day).toBe(expected); + }); + + it('returns text in the "Today is " format', async () => { + const result = await run({}); + expect(result.text).toBe(`Today is ${result.day}`); + }); + + it('returns one of the seven valid weekday names', async () => { + const validDays = [ + 'Sunday', 'Monday', 'Tuesday', 'Wednesday', + 'Thursday', 'Friday', 'Saturday' + ]; + const result = await run({}); + expect(validDays).toContain(result.day); + }); + + it('ignores unexpected input rather than throwing', async () => { + // The schema declares additionalProperties: false, but that's + // enforced by the MCP server's validator before dispatch, not by + // the tool function itself — the tool should still be defensive. + await expect(run({} as never)).resolves.toBeDefined(); + }); +}); + +describe('day.get across known dates', () => { + const realDateNow = Date; + + function mockDate(iso: string) { + class MockDate extends Date { + constructor() { + super(iso); + } + } + // @ts-expect-error - intentional global override for testing + global.Date = MockDate; + } + + afterEach(() => { + global.Date = realDateNow; + }); + + it('resolves a Monday correctly', async () => { + mockDate('2026-07-20T12:00:00Z'); // confirmed Monday + const result = await run({}); + expect(result.day).toBe('Monday'); + expect(result.text).toBe('Today is Monday'); + }); + + it('resolves a Sunday correctly', async () => { + mockDate('2026-07-19T12:00:00Z'); // confirmed Sunday + const result = await run({}); + expect(result.day).toBe('Sunday'); + }); +}); + +describe('day.get schema', () => { + it('declares an object schema with no required properties', () => { + expect(inputSchema.type).toBe('object'); + expect(inputSchema.properties).toEqual({}); + }); + + it('rejects additional properties', () => { + expect(inputSchema.additionalProperties).toBe(false); + }); +}); \ No newline at end of file diff --git a/packages/tools/email-tool/README.md b/packages/tools/email-tool/README.md new file mode 100644 index 00000000..f562a1e6 --- /dev/null +++ b/packages/tools/email-tool/README.md @@ -0,0 +1,3 @@ +This email tool is a legacy tool that is coupled to its own server, and does not work with CODA2's MCP v2 compatible tool server. + +It is not loaded, not run, and should not be used. \ No newline at end of file diff --git a/packages/tools/email-tool/config/contacts.txt b/packages/tools/email-tool/config/contacts.txt new file mode 100755 index 00000000..c025149d --- /dev/null +++ b/packages/tools/email-tool/config/contacts.txt @@ -0,0 +1,16 @@ +computekie@gmail.com +Mars Group +Gilad Penn +Hannah Onstad +Isabella Mariè +Jake +Peter Kreutlein +Natasha Pelak +Serene +Varun Gangal +Yang Paul +Zak +Zoe Braiterman +Zeta Jones +NewYork ctoLunches + diff --git a/packages/tools/email-tool/config/llm_sigs.json b/packages/tools/email-tool/config/llm_sigs.json new file mode 100644 index 00000000..492de97d --- /dev/null +++ b/packages/tools/email-tool/config/llm_sigs.json @@ -0,0 +1,10 @@ +{ + "llm_signatures": [ + "Written by a language model\nThat I actually own\nWild concept, I know", + "Sent via my personal AI overlord. \n(Yes, a local LLM wrote this. No, I didn't proofread it.)", + "Drafted by silicon, approved by carbon\nRunning locally because I have trust issues", + "Composed by an LLM that doesn't phone home\nUnlike literally everything else you use", + "Sent from my local LLM\nBecause I don't trust yours", + "Sent from my local LLM\n(Yes, it runs on my machine. No, you can't have my data.)" + ] +} diff --git a/packages/tools/email-tool/config/sig.txt b/packages/tools/email-tool/config/sig.txt new file mode 100644 index 00000000..200a16bf --- /dev/null +++ b/packages/tools/email-tool/config/sig.txt @@ -0,0 +1,10 @@ + +Best, + +Forest Mars +-- +Sent via my personal AI overlord +(Yes, a local LLM wrote this. No, I didn't proofread it.) + +Compustretch: The machine that thinks about doing it for you +Powered by Polyglot diff --git a/packages/tools/email-tool/config/sig2.txt b/packages/tools/email-tool/config/sig2.txt new file mode 100644 index 00000000..925f7357 --- /dev/null +++ b/packages/tools/email-tool/config/sig2.txt @@ -0,0 +1,12 @@ + +Best, + +Forest Mars +-- +Sent via my personal AI overlord +(Yes, a local LLM wrote this. No, I didn't proofread it.) + +My AI Blog: mlops.nyc + +Compustretch: The machine that thinks about doing it for you +Powered by Polyglot diff --git a/packages/tools/email-tool/config/tool-descript.txt b/packages/tools/email-tool/config/tool-descript.txt new file mode 100644 index 00000000..bddcc06c --- /dev/null +++ b/packages/tools/email-tool/config/tool-descript.txt @@ -0,0 +1,33 @@ +Send an email via Gmail SMTP. Use parameter name "to" for recipient address. + +PARAMETERS: +- to: Recipient email address(es). Can be a single email string OR an array of email strings for multiple recipients + Examples: "alice@example.com" OR ["alice@example.com", "bob@example.com"] +- subject: Email subject line +- body: Email body content + +CRITICAL: Always format email bodies with proper paragraph spacing. Insert \n\n (double newline) between each paragraph or separate thought. Single-paragraph emails are difficult to read. +Note: A signature is automatically appended to all emails. Do not include signature placeholders like "[Your Name]" in the body. + +IMPORTANT WORKFLOW: +1. Before calling this tool, draft the email and show it to the user +2. Ask "Should I send this email? Reply 'yes' to confirm." +3. Only call send_email() after the user explicitly confirms with 'yes' or 'send' + +Never send emails without user confirmation unless the user explicitly requests it. + +To call a tool, use this EXACT format: +```tool_code +send_email(to="recipient@example.com", subject="Subject", body="Body text") +``` + +EXAMPLE: +User: Send an email to john@example.com with subject "Hello" +Assistant: [drafts and gets confirmation] +User: yes +Assistant: +```tool_code +send_email(to="john@example.com", subject="Hello", body="The body text") +``` + +Use these tools when they can help answer the user's question. diff --git a/packages/tools/email-tool/package-lock.json b/packages/tools/email-tool/package-lock.json new file mode 100644 index 00000000..585bcc22 --- /dev/null +++ b/packages/tools/email-tool/package-lock.json @@ -0,0 +1,1498 @@ +{ + "name": "email-tool", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "model-context-protocol": "^1.0.2" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.21.1.tgz", + "integrity": "sha512-UyLFcJLDvUuZbGnaQqXFT32CpPpGj7VS19roLut6gkQVhb439xUzYWbsUvdI3ZPL+2hnFosuugtYWE0Mcs1rmQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.0.1", + "express-rate-limit": "^7.5.0", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.23.8", + "zod-to-json-schema": "^3.24.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/express/node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/model-context-protocol": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/model-context-protocol/-/model-context-protocol-1.0.2.tgz", + "integrity": "sha512-8JFpC3hGx21anEp8BCCTQhnPPrvpE1/rxUC/pJg8JPmEoZqYr3PROCiuo3JB1akzudFriXOztKmBN2gcv7ksYQ==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.6.1", + "express": "^4.21.2", + "zod": "^3.24.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pkce-challenge": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz", + "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", + "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.7.0", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/router/node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + } + } +} diff --git a/packages/tools/email-tool/package.json b/packages/tools/email-tool/package.json new file mode 100644 index 00000000..ac985b5e --- /dev/null +++ b/packages/tools/email-tool/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "model-context-protocol": "^1.0.2" + } +} diff --git a/packages/tools/email-tool/server.ts b/packages/tools/email-tool/server.ts new file mode 100644 index 00000000..46c19952 --- /dev/null +++ b/packages/tools/email-tool/server.ts @@ -0,0 +1,78 @@ +// server.ts +import { WebSocket, WebSocketServer } from 'ws'; +import { sendEmail } from './src/gmail.ts'; +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Load signature once at startup +const EMAIL_SIGNATURE = readFileSync(join(__dirname, 'config', 'sig.txt'), 'utf-8'); + +// Here's where the magic hapens, obvi. +const SEND_EMAIL_DESCRIPTION = readFileSync(join(__dirname, 'config', 'tool-descript.txt'), 'utf-8'); + +const wss = new WebSocketServer({ port: 9002 }); + +console.log('Email MCP Server running on ws://localhost:9002'); +console.log('Loaded email signature:', EMAIL_SIGNATURE.substring(0, 50) + '...'); + +wss.on('connection', (ws) => { + ws.on('message', async (data) => { + const msg = JSON.parse(data); + + if (msg.method === 'tools/list') { + ws.send(JSON.stringify({ + jsonrpc: '2.0', + id: msg.id, + result: { + tools: [{ + name: 'send_email', + description: SEND_EMAIL_DESCRIPTION, + inputSchema: { + type: 'object', + properties: { + to: { + oneOf: [ + { type: 'string', description: 'Single recipient email address' }, + { type: 'array', items: { type: 'string' }, description: 'Multiple recipient email addresses' } + ], + description: 'Recipient email address' }, + subject: { type: 'string', description: 'Email subject' }, + body: { type: 'string', description: 'Email body' } + }, + required: ['to', 'subject', 'body'] + } + }] + } + })); + } + + if (msg.method === 'tools/call' && msg.params.name === 'send_email') { + try { + await sendEmail({ ...msg.params.arguments, signature: EMAIL_SIGNATURE }); + ws.send(JSON.stringify({ + jsonrpc: '2.0', + id: msg.id, + result: { + content: [{ + type: 'text', + text: `Email sent to ${msg.params.arguments.to}` + }] + } + })); + } catch (error) { + ws.send(JSON.stringify({ + jsonrpc: '2.0', + id: msg.id, + error: { + code: -1, + message: error.message + } + })); + } + } + }); +}); \ No newline at end of file diff --git a/packages/tools/email-tool/src/contacts.ts b/packages/tools/email-tool/src/contacts.ts new file mode 100644 index 00000000..6eb247b5 --- /dev/null +++ b/packages/tools/email-tool/src/contacts.ts @@ -0,0 +1,151 @@ +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +interface Contact { + name: string; + email: string; +} + +export class ContactsService { + private contactsPath: string; + + constructor() { + this.contactsPath = join(__dirname, '..', 'config', 'contacts.txt'); + } + + private loadContacts(): Contact[] { + if (!existsSync(this.contactsPath)) { + return []; + } + + const content = readFileSync(this.contactsPath, 'utf-8'); + const contacts: Contact[] = []; + + for (const line of content.split('\n')) { + const trimmed = line.trim(); + if (!trimmed) continue; + + // Parse "Name " format + const match = trimmed.match(/^(.+?)\s*<([^>]+@[^>]+)>$/); + if (match) { + contacts.push({ + name: match[1].trim(), + email: match[2].trim() + }); + } + } + + return contacts; + } + + /** + * Resolve a recipient string to RFC 5322 format: "Name " + * + * Accepts: + * - "Zeta" (first name lookup) + * - "Zeta Jones" (full name lookup) + * - "lostjournals" (email username lookup) + * - "lostjournals@gmail.com" (direct email) + * - "Zeta Jones " (already formatted) + * - "Zeta Jones lostjournals@gmail.com" (name + email without brackets) + */ +resolve(input: string): string { + const trimmed = input.trim(); + const contacts = this.loadContacts(); + + // If already in RFC 5322 format with angle brackets, check and add if needed + const rfc5322Match = trimmed.match(/^(.+?)\s*<([^>]+@[^>]+)>$/); + if (rfc5322Match) { + const name = rfc5322Match[1].trim(); + const email = rfc5322Match[2].trim(); + + const contact = contacts.find(c => c.email.toLowerCase() === email.toLowerCase()); + if (!contact) { + this.addContact(name, email); + } + + return trimmed; + } + + // If format is "Name email@example.com" (without angle brackets), normalize and add if needed + const nameEmailMatch = trimmed.match(/^(.+?)\s+([^\s]+@[^\s]+)$/); + if (nameEmailMatch) { + const name = nameEmailMatch[1].trim(); + const email = nameEmailMatch[2].trim(); + + const contact = contacts.find(c => c.email.toLowerCase() === email.toLowerCase()); + if (!contact) { + this.addContact(name, email); + } + + return `${name} <${email}>`; + } + + // If it's just an email address, check and add if needed + if (trimmed.includes('@')) { + const email = trimmed; + const contact = contacts.find(c => c.email.toLowerCase() === email.toLowerCase()); + if (contact) { + return `${contact.name} <${contact.email}>`; + } + // Email not in contacts - add it with email as the name + this.addContact(email, email); + return email; + } + + // Try to find by first name + const byFirstName = contacts.find(c => + c.name.split(' ')[0].toLowerCase() === trimmed.toLowerCase() + ); + if (byFirstName) { + return `${byFirstName.name} <${byFirstName.email}>`; + } + + // Try to find by full name + const byFullName = contacts.find(c => + c.name.toLowerCase() === trimmed.toLowerCase() + ); + if (byFullName) { + return `${byFullName.name} <${byFullName.email}>`; + } + + // Try to find by email username (part before @) + const byUsername = contacts.find(c => + c.email.split('@')[0].toLowerCase() === trimmed.toLowerCase() + ); + if (byUsername) { + return `${byUsername.name} <${byUsername.email}>`; + } + + // Not found - throw error + throw new Error(`Contact not found: "${input}"`); +} + + /** + * Add a new contact to the contacts file + */ + addContact(name: string, email: string): void { + const contacts = this.loadContacts(); + + // Check if contact already exists + const exists = contacts.find(c => + c.email.toLowerCase() === email.toLowerCase() + ); + + if (exists) { + console.log(`Contact ${email} already exists`); + return; + } + + // Append to file + const line = `${name} <${email}>\n`; + writeFileSync(this.contactsPath, line, { flag: 'a' }); + console.log(`Added contact: ${name} <${email}>`); + } +} + +export const contactsService = new ContactsService(); diff --git a/packages/tools/email-tool/src/gmail.test.ts b/packages/tools/email-tool/src/gmail.test.ts new file mode 100644 index 00000000..90813222 --- /dev/null +++ b/packages/tools/email-tool/src/gmail.test.ts @@ -0,0 +1,20 @@ +import { sendEmail } from "./gmail"; +import { SendEmailInput } from "./schema"; + +async function testSendEmail() { + const email: SendEmailInput = { + to: "themarsgroup@gmail.com", // replace with your test recipient + subject: "SMTP Test Email", + body: "This is a test email sent directly from gmail.ts", + }; + + try { + await sendEmail(email); + console.log("gmail.ts test succeeded!"); + } catch (err) { + console.error("gmail.ts test failed:", err); + } +} + +testSendEmail(); + diff --git a/packages/tools/email-tool/src/gmail.ts b/packages/tools/email-tool/src/gmail.ts new file mode 100644 index 00000000..ccda0aed --- /dev/null +++ b/packages/tools/email-tool/src/gmail.ts @@ -0,0 +1,67 @@ +// email-tool/src/gmail.ts +import nodemailer from "nodemailer"; +import { sendEmailSchema, SendEmailInput } from "./schema"; +import { contactsService } from "./contacts"; +import { readFileSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Load credentials from .auth file +const authPath = join(__dirname, '..', '.auth'); +const authContent = readFileSync(authPath, 'utf-8'); + +const authLines = authContent.split('\n'); +const EMAIL_ADDRESS = authLines.find(line => line.startsWith('EMAIL_ADDRESS='))?.split('=')[1]?.trim() || ''; +const EMAIL_PASSWORD = authLines.find(line => line.startsWith('EMAIL_PASSWORD='))?.split('=')[1]?.trim() || ''; + +if (!EMAIL_ADDRESS || !EMAIL_PASSWORD) { + throw new Error('Missing EMAIL_ADDRESS or EMAIL_PASSWORD in .auth file'); +} + +export async function sendEmail(input: SendEmailInput & { signature?: string }) { + const message = sendEmailSchema.parse(input); + + // Resolve contacts - handle both string and array + let recipients: string; + if (Array.isArray(message.to)) { + recipients = message.to.map(t => contactsService.resolve(t)).join(', '); + } else { + recipients = contactsService.resolve(message.to); + } + + const transporter = nodemailer.createTransport({ + service: "gmail", + auth: { + user: EMAIL_ADDRESS, + pass: EMAIL_PASSWORD, + }, + }); + + // Convert both literal \n and escaped \\n to
tags for HTML email + const bodyWithBreaks = message.body + .replace(/\\n/g, '\n') + .replace(/\n/g, '
'); + + const signatureWithBreaks = input.signature + ? input.signature.replace(/\n/g, '
') + : ''; + + const aiDisclosure = "

I'm Compustretch, Forest's AI assistant.

"; + + const htmlBodyWithSignature = input.signature + ? `${aiDisclosure}${bodyWithBreaks}${signatureWithBreaks}` + : `${aiDisclosure}${bodyWithBreaks}`; + + await transporter.sendMail({ + from: EMAIL_ADDRESS, + to: recipients, + cc: "mars@mlops.nyc", + subject: message.subject, + html: htmlBodyWithSignature, + }); + + console.log(`Email sent to ${recipients}`); +} \ No newline at end of file diff --git a/packages/tools/email-tool/src/mcp.test.ts b/packages/tools/email-tool/src/mcp.test.ts new file mode 100644 index 00000000..d0a5d4d6 --- /dev/null +++ b/packages/tools/email-tool/src/mcp.test.ts @@ -0,0 +1,15 @@ +import { sendEmail } from "./gmail"; +import { SendEmailInput } from "./schema"; + +async function main() { + const email: SendEmailInput = { + to: "themarsgroup@gmail.com", + subject: "Test Email 2 from Local Client", + body: "This is a test email sent via local SMTP, no Google Cloud needed.", + }; + + await sendEmail(email); +} + +main().catch(console.error); + diff --git a/packages/tools/email-tool/src/mcp.ts b/packages/tools/email-tool/src/mcp.ts new file mode 100644 index 00000000..089e07be --- /dev/null +++ b/packages/tools/email-tool/src/mcp.ts @@ -0,0 +1,86 @@ +// toos/email-tool/src/mcp.ts +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { + CallToolRequestSchema, + ListToolsRequestSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { sendEmail } from "./gmail.js"; +import { SendEmailInput } from "./schema.js"; + +// Create MCP server +const server = new Server( + { + name: "gmail-email-tool", + version: "1.0.0", + }, + { + capabilities: { + tools: {}, + }, + } +); + +// Register the list_tools handler +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: "send_email", + description: "Send an email via Gmail SMTP", + inputSchema: { + type: "object", + properties: { + to: { type: "string", description: "Recipient email address" }, + subject: { type: "string", description: "Email subject" }, + body: { type: "string", description: "Email body" }, + }, + required: ["to", "subject", "body"], + }, + }, + ], + }; +}); + +// Register the call_tool handler +server.setRequestHandler(CallToolRequestSchema, async (request) => { + if (request.params.name === "send_email") { + try { + const input = request.params.arguments as SendEmailInput; + await sendEmail(input); + + return { + content: [ + { + type: "text", + text: `Email sent successfully to ${input.to}`, + }, + ], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: `Error sending email: ${error instanceof Error ? error.message : "Unknown error"}`, + }, + ], + isError: true, + }; + } + } + + throw new Error(`Unknown tool: ${request.params.name}`); +}); + +// Start the server +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("Gmail MCP Server running on stdio"); +} + +main().catch((error) => { + console.error("Server error:", error); + process.exit(1); +}); \ No newline at end of file diff --git a/packages/tools/email-tool/src/schema.ts b/packages/tools/email-tool/src/schema.ts new file mode 100644 index 00000000..c3d1a74a --- /dev/null +++ b/packages/tools/email-tool/src/schema.ts @@ -0,0 +1,13 @@ +// email-tool/src/schema.ts +import { z } from 'zod'; + +export const sendEmailSchema = z.object({ + to: z.union([ + z.string().min(1), // Accept any string - contacts service will resolve + z.array(z.string().min(1)).min(1) + ]), + subject: z.string().min(1), + body: z.string().min(1) +}); + +export type SendEmailInput = z.infer; \ No newline at end of file diff --git a/packages/tools/email-tool/tsconfig.json b/packages/tools/email-tool/tsconfig.json new file mode 100644 index 00000000..d0302722 --- /dev/null +++ b/packages/tools/email-tool/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} + diff --git a/packages/tools/fs/bash/manifest.json b/packages/tools/fs/bash/manifest.json index a2b66f25..95dea2aa 100644 --- a/packages/tools/fs/bash/manifest.json +++ b/packages/tools/fs/bash/manifest.json @@ -1,5 +1,5 @@ { - "name": "fs/bash", + "name": "fs.bash", "description": "Run a shell command in the project root directory.", "entry": "index.ts", "parameters": { diff --git a/packages/tools/fs/glob/manifest.json b/packages/tools/fs/glob/manifest.json index 964fe473..09008cb3 100644 --- a/packages/tools/fs/glob/manifest.json +++ b/packages/tools/fs/glob/manifest.json @@ -1,5 +1,5 @@ { - "name": "fs/glob", + "name": "fs.glob", "description": "List files in the project matching a pattern, e.g. *.ts or *.json.", "entry": "index.ts", "parameters": { diff --git a/packages/tools/fs/read/manifest.json b/packages/tools/fs/read/manifest.json index a4c4a842..0b8bbe79 100644 --- a/packages/tools/fs/read/manifest.json +++ b/packages/tools/fs/read/manifest.json @@ -1,5 +1,5 @@ { - "name": "fs/read", + "name": "fs.read", "description": "Read the content of a file. The path must be relative to the project root, e.g. src/main.ts or README.md.", "entry": "index.ts", "parameters": { diff --git a/packages/tools/fs/write/manifest.json b/packages/tools/fs/write/manifest.json index 8679a9df..e88e5ccc 100644 --- a/packages/tools/fs/write/manifest.json +++ b/packages/tools/fs/write/manifest.json @@ -1,5 +1,5 @@ { - "name": "fs/write", + "name": "fs.write", "description": "Write content to a file. The path must be relative to the project root, e.g. src/main.ts or README.md. Never use absolute paths or placeholder paths like /path/to/file.", "entry": "index.ts", "parameters": { diff --git a/packages/tools/github/close_issue/manifest.json b/packages/tools/github/close_issue/manifest.json index e69de29b..96fe77ed 100644 --- a/packages/tools/github/close_issue/manifest.json +++ b/packages/tools/github/close_issue/manifest.json @@ -0,0 +1,12 @@ +{ + "name": "github.closes_issue", + "description": "Closes an existing issue in a GitHub repository.", + "entry": "./index.ts", + "parameters": { + "type": "object", + "properties": { + "issue_number": { "type": "number", "description": "The issue number to close" } + }, + "required": ["issue_number"] + } +} diff --git a/packages/tools/github/comment_issue/manifest.json b/packages/tools/github/comment_issue/manifest.json index 6d953d04..7eac7e53 100644 --- a/packages/tools/github/comment_issue/manifest.json +++ b/packages/tools/github/comment_issue/manifest.json @@ -1,5 +1,5 @@ { - "name": "github/comment_issue", + "name": "github.comment_issue", "description": "Adds a comment to an existing GitHub issue.", "entry": "./index.ts", "parameters": { diff --git a/packages/tools/github/create_issue/manifest.json b/packages/tools/github/create_issue/manifest.json index f064dd10..5bb50877 100644 --- a/packages/tools/github/create_issue/manifest.json +++ b/packages/tools/github/create_issue/manifest.json @@ -1,5 +1,5 @@ { - "name": "github/create_issue", + "name": "github.create_issue", "description": "Create a new GitHub issue in the configured repository. REQUIRED: title. OPTIONAL: body (description), labels (tags for the issue).", "entry": "./index.ts", "parameters": { diff --git a/packages/tools/github/search_issues/manifest.json b/packages/tools/github/search_issues/manifest.json index ba6315ed..8dac0194 100644 --- a/packages/tools/github/search_issues/manifest.json +++ b/packages/tools/github/search_issues/manifest.json @@ -1,5 +1,5 @@ { - "name": "github/search_issues", + "name": "github.search_issues", "description": "Search for GitHub issues in the repository.", "entry": "./index.ts", "parameters": { diff --git a/packages/tools/index.ts b/packages/tools/index.ts index a5eb2bc3..79a3f396 100644 --- a/packages/tools/index.ts +++ b/packages/tools/index.ts @@ -1,6 +1,6 @@ /** - * @file index.ts - * @description Central registry for all tools. + * @file packages/tools/index.ts + * @description Central registry for all support tools. */ import registry from "./registry.json" assert { type: "json" }; import { runTool } from "./loader"; diff --git a/packages/tools/loader.ts b/packages/tools/loader.ts index 0b7b7024..aeee3767 100644 --- a/packages/tools/loader.ts +++ b/packages/tools/loader.ts @@ -1,3 +1,4 @@ +// packages/tools/loader.ts import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; import registry from "./registry.json" assert { type: "json" }; diff --git a/packages/tools/mcpServer.ts b/packages/tools/mcpServer.ts new file mode 100644 index 00000000..202780e1 --- /dev/null +++ b/packages/tools/mcpServer.ts @@ -0,0 +1,364 @@ +// MCP Streamable HTTP server — Bun + @modelcontextprotocol/server (v2, beta) +// Confirmed installed: @modelcontextprotocol/server@2.0.0-beta.4 +// +// Uses createMcpHandler, the SDK's documented entry point for HTTP +// deployments: it serves 2026-07-28 per request and falls back to +// 2025-11-25 handshake behavior for older clients on the same endpoint. +// handler.fetch is (Request) => Promise, so it plugs into +// Bun.serve directly with no framework in between. +// +// REQUIRES: bun add @cfworker/json-schema (confirmed NOT currently +// installed — fromJsonSchema() will throw at import until this is added) +// +// Run: bun run mcpServer.ts + +// MCP Streamable HTTP server — Bun + @modelcontextprotocol/server (v2, beta) +// Complete Replacement with Robust Schema Guardrails & Debugging +import path from 'node:path'; + +import { + McpServer, + createMcpHandler, + fromJsonSchema, + hostHeaderValidationResponse, + originValidationResponse, + localhostAllowedHostnames, + localhostAllowedOrigins +} from '@modelcontextprotocol/server'; + +import registryData from './registry.json'; + +// ============================================================================ +// DEBUG LOGGING CONFIGURATION & HELPERS +// ============================================================================ +const DEBUG = true; // Set to false to disable verbose debug logging + +function logDebug(section: string, message: string, data?: unknown) { + if (!DEBUG) return; + const timestamp = new Date().toISOString(); + const prefix = `[DEBUG ${timestamp}] [${section}]`; + if (data !== undefined) { + console.log(`${prefix} ${message}\n`, typeof data === 'object' ? JSON.stringify(data, null, 2) : data); + } else { + console.log(`${prefix} ${message}`); + } +} + +function logError(section: string, message: string, error?: unknown) { + const timestamp = new Date().toISOString(); + const prefix = `[ERROR ${timestamp}] [${section}]`; + if (error instanceof Error) { + console.error(`${prefix} ${message}\nStack: ${error.stack}`); + } else if (error !== undefined) { + console.error(`${prefix} ${message}`, error); + } else { + console.error(`${prefix} ${message}`); + } +} + +// Global Process Diagnostics & Failure Tracing +process.on('uncaughtException', (err) => { + logError('PROCESS', 'Uncaught Exception detected!', err); +}); + +process.on('unhandledRejection', (reason, promise) => { + logError('PROCESS', `Unhandled Rejection at: ${promise}`, reason); +}); + +// Startup Environment Diagnostics +logDebug('STARTUP', 'Initializing MCP Server Environment', { + bunVersion: Bun.version, + nodeVersion: process.version, + cwd: process.cwd(), + execPath: process.execPath, + pid: process.pid, + envPort: process.env.PORT ?? '5555 (default)' +}); + +// ============================================================================ +// REGISTRY & TOOL LOADING +// ============================================================================ +interface RegistryEntry { + name: string; + description: string; + entry: string; + inputSchema: Record; // raw JSON Schema from manifest.json + importPath: string; // relative to packages/tools +} + +const registry = registryData as RegistryEntry[]; +const TOOLS_ROOT = path.join(process.cwd(), 'packages/tools'); + +logDebug('REGISTRY', 'Loaded raw registry.json', { + toolsRootResolvedPath: TOOLS_ROOT, + totalEntriesCount: registry.length, + rawEntryNames: registry.map((r) => r.name) +}); + +type ToolFn = (args: unknown) => Promise | unknown; + +interface LoadedTool extends RegistryEntry { + fn: ToolFn; +} + +/** + * Normalizes raw tool parameter schema to ensure AJV / fromJsonSchema receive a valid object. + */ +function sanitizeSchema(params: unknown, toolName: string): Record { + if (!params || typeof params !== 'object' || Array.isArray(params)) { + logDebug('SCHEMA_GUARD', `Tool "${toolName}" missing valid parameters schema object. Fallback to empty object schema.`); + return { + type: 'object', + properties: {} + }; + } + + const schemaObj = { ...(params as Record) }; + + // Ensure fundamental JSON schema fields exist if missing + if (!schemaObj.type) { + schemaObj.type = 'object'; + } + if (!schemaObj.properties && schemaObj.type === 'object') { + schemaObj.properties = {}; + } + + return schemaObj; +} + +async function loadTool(entry: RegistryEntry): Promise { + const fullImportPath = path.join(TOOLS_ROOT, entry.importPath); + logDebug('TOOL_LOAD', `Attempting import for tool "${entry.name}"`, { + importPathRelative: entry.importPath, + resolvedFullPath: fullImportPath + }); + + try { + const mod = await import(fullImportPath); + const exportKeys = Object.keys(mod); + logDebug('TOOL_LOAD', `Export discovery for "${entry.name}"`, { availableExports: exportKeys }); + + const fn: ToolFn | undefined = typeof mod.run === 'function' ? mod.run : undefined; + + if (!fn) { + throw new Error( + `Tool "${entry.name}" (${entry.importPath}) does not export a "run" function. ` + + `Found exports: [${exportKeys.join(', ')}]. Confirmed convention is: export async function run(params).` + ); + } + + logDebug('TOOL_LOAD', `Successfully loaded function for tool "${entry.name}"`); + return { ...entry, fn }; + } catch (err) { + logError('TOOL_LOAD', `Failed to load tool "${entry.name}" from path "${fullImportPath}"`, err); + throw err; + } +} + +let loadedToolsPromise: Promise | undefined; + +function getLoadedTools(): Promise { + if (!loadedToolsPromise) { + loadedToolsPromise = (async () => { + logDebug('REGISTRY', 'Starting tool batch load and duplicate check...'); + + // Duplicate check + const seenNames = new Set(); + const duplicates: string[] = []; + for (const entry of registry) { + if (seenNames.has(entry.name)) { + duplicates.push(entry.name); + } + seenNames.add(entry.name); + } + + if (duplicates.length > 0) { + const errStr = `Duplicate tool names detected in registry: [${duplicates.join(', ')}]`; + logError('REGISTRY', errStr); + throw new Error(errStr); + } + + const results = await Promise.all(registry.map(loadTool)); + logDebug('REGISTRY', `All ${results.length} tools successfully loaded and validated.`, { + registeredNames: results.map((t) => t.name) + }); + return results; + })(); + } + return loadedToolsPromise; +} + +// ============================================================================ +// SERVER FACTORY & TOOL EXECUTION +// ============================================================================ +async function buildServer(): Promise { + // logDebug('SERVER_BUILD', 'buildServer() invoked (constructing new McpServer instance)'); // much higher debug level! + const server = new McpServer({ name: 'coda2-tools-server', version: '1.0.0' }); + + const tools = await getLoadedTools(); + + console.log(JSON.stringify(tools, null, 2)); + for (const tool of tools) { + const safeSchema = sanitizeSchema(tool.inputSchema, tool.name); + + logDebug('SERVER_BUILD', `Registering tool "${tool.name}" with McpServer instance`, { + rawParameters: tool.inputSchema, + sanitizedSchema: safeSchema + }); + + // Sanitize name to strictly conform to MCP naming standard (A-Z, a-z, 0-9, _, -, .) + const safeToolName = tool.name.replace(/\//g, '_'); + + server.registerTool( + safeToolName, + { + title: tool.name, + description: tool.description, + inputSchema: fromJsonSchema(safeSchema) + }, + + async (args: unknown) => { + const startTime = performance.now(); + logDebug('TOOL_EXEC', `Executing tool "${tool.name}"`, { inputArgs: args }); + + try { + const result = await tool.fn(args); + const durationMs = (performance.now() - startTime).toFixed(2); + + logDebug('TOOL_EXEC', `Tool "${tool.name}" executed successfully in ${durationMs}ms`, { + resultOutput: result + }); + + return { + content: [{ type: 'text' as const, text: JSON.stringify(result) }] + }; + } catch (error) { + const durationMs = (performance.now() - startTime).toFixed(2); + logError('TOOL_EXEC', `Tool "${tool.name}" failed after ${durationMs}ms`, error); + + return { + content: [ + { + type: 'text' as const, + text: `Failed: ${error instanceof Error ? error.message : String(error)}` + } + ], + isError: true + }; + } + } + ); + } + + logDebug('SERVER_BUILD', 'McpServer instance successfully built and populated with tools.'); + return server; +} + +const handler = createMcpHandler(buildServer, { + onerror: (err: unknown) => { + logError('HANDLER_ERROR', 'MCP streamable handler encountered error', err); + } +}); + +// ============================================================================ +// HTTP SERVER & REQUEST / RESPONSE TRACING +// ============================================================================ +const MCP_PATH = '/mcp'; +const PORT = Number(process.env.PORT ?? 5555); + +const server = Bun.serve({ + port: PORT, + async fetch(req) { + const reqId = Math.random().toString(36).substring(2, 9); + const startTime = performance.now(); + const url = new URL(req.url); + + const sessionId = req.headers.get('mcp-session-id') || req.headers.get('x-session-id') || 'none'; + + logDebug('HTTP_REQ', `[${reqId}] Incoming ${req.method} ${url.pathname}`, { + method: req.method, + pathname: url.pathname, + searchParams: Object.fromEntries(url.searchParams.entries()), + headers: { + host: req.headers.get('host'), + origin: req.headers.get('origin'), + contentType: req.headers.get('content-type'), + accept: req.headers.get('accept'), + sessionId: sessionId + } + }); + + if (url.pathname !== MCP_PATH) { + logDebug('HTTP_REQ', `[${reqId}] Path ${url.pathname} !== ${MCP_PATH}, returning 404`); + return new Response('Not found', { status: 404 }); + } + + // Validate headers + const hostValidation = hostHeaderValidationResponse(req, localhostAllowedHostnames()); + if (hostValidation) { + logError('SECURITY', `[${reqId}] Host header validation rejected request`, req.headers.get('host')); + return hostValidation; + } + + const originValidation = originValidationResponse(req, localhostAllowedOrigins()); + if (originValidation) { + logError('SECURITY', `[${reqId}] Origin header validation rejected request`, req.headers.get('origin')); + return originValidation; + } + + // Trace request payload if present + let clonedReq = req; + if (req.method === 'POST' && DEBUG) { + try { + const reqClone = req.clone(); + const bodyText = await reqClone.text(); + try { + const parsedJson = JSON.parse(bodyText); + logDebug('JSONRPC_REQ', `[${reqId}] Parsed JSON-RPC Request Payload:`, parsedJson); + } catch { + logDebug('HTTP_REQ', `[${reqId}] Raw Post Body (non-JSON):`, bodyText); + } + } catch (err) { + logError('HTTP_REQ', `[${reqId}] Failed to clone/read request body`, err); + } + } + + try { + const response = await handler.fetch(clonedReq); + const durationMs = (performance.now() - startTime).toFixed(2); + + logDebug('HTTP_RES', `[${reqId}] Response dispatched in ${durationMs}ms`, { + status: response.status, + statusText: response.statusText, + contentType: response.headers.get('content-type'), + sessionId: response.headers.get('mcp-session-id') || response.headers.get('x-session-id') || sessionId + }); + + return response; + } catch (fetchErr) { + const durationMs = (performance.now() - startTime).toFixed(2); + logError('HTTP_REQ', `[${reqId}] handler.fetch threw an exception after ${durationMs}ms`, fetchErr); + return new Response('Internal Server Error', { status: 500 }); + } + } +}); + +// Fail fast at boot if any tool module is missing or malformed. +logDebug('BOOT', 'Pre-loading tools to ensure fast failure on startup errors...'); +await getLoadedTools(); + +console.log( + `\n🚀 MCP Streamable HTTP server listening on http://localhost:${server.port}${MCP_PATH}` + + `\n Debug Logging Enabled: ${DEBUG}\n` +); + +process.on('SIGINT', async () => { + logDebug('SHUTDOWN', 'SIGINT signal received. Closing handler and shutting down...'); + try { + await handler.close(); + logDebug('SHUTDOWN', 'Handler closed cleanly.'); + } catch (err) { + logError('SHUTDOWN', 'Error while closing handler during shutdown', err); + } + process.exit(0); +}); \ No newline at end of file diff --git a/packages/tools/package.json b/packages/tools/package.json index 1c253db5..0934aaba 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -12,5 +12,9 @@ }, "./order-tools": "./order-tools.ts", "./tools": "./tools/index.ts" + }, + "scripts": { + "server": "bun run mcpServer.ts" } } + diff --git a/packages/tools/registry.json b/packages/tools/registry.json index 0fc07a84..30a066df 100644 --- a/packages/tools/registry.json +++ b/packages/tools/registry.json @@ -1,84 +1,70 @@ [ { - "name": "smoke", - "description": "A sanity check tool to verify tool-calling is working. Triggered when the user asks what day it is.", - "entry": "index.ts", - "modes": [ - "support" - ], - "parameters": { + "name": "day.get", + "description": "Returns the current day of the week", + "importPath": "day-tool/index.ts", + "inputSchema": { "type": "object", - "properties": { - "query": { - "type": "string", - "description": "The user's question about the date or time" - } - }, - "required": [ - "query" - ] - }, - "importPath": "smoke/index.ts" + "properties": {}, + "additionalProperties": false + } }, { - "name": "fs/read", - "description": "Read the content of a file. The path must be relative to the project root, e.g. src/main.ts or README.md.", - "entry": "index.ts", - "parameters": { + "name": "fs.bash", + "description": "Run a shell command in the project root directory.", + "importPath": "fs/bash/index.ts", + "inputSchema": { "type": "object", "properties": { - "path": { + "command": { "type": "string", - "description": "Relative path from the project root, e.g. src/main.ts" + "description": "The shell command to run, e.g. npm test or git status" } }, "required": [ - "path" + "command" ] - }, - "importPath": "fs/read/index.ts" + } }, { - "name": "fs/bash", - "description": "Run a shell command in the project root directory.", - "entry": "index.ts", - "parameters": { + "name": "fs.glob", + "description": "List files in the project matching a pattern, e.g. *.ts or *.json.", + "importPath": "fs/glob/index.ts", + "inputSchema": { "type": "object", "properties": { - "command": { + "pattern": { "type": "string", - "description": "The shell command to run, e.g. npm test or git status" + "description": "Glob pattern to match, e.g. *.ts or src/**/*.ts" } }, "required": [ - "command" + "pattern" ] - }, - "importPath": "fs/bash/index.ts" + } }, { - "name": "fs/glob", - "description": "List files in the project matching a pattern, e.g. *.ts or *.json.", - "entry": "index.ts", - "parameters": { + "name": "fs.read", + "description": "Read the content of a file. The path must be relative to the project root, e.g. src/main.ts or README.md.", + "importPath": "fs/read/index.ts", + "inputSchema": { "type": "object", "properties": { - "pattern": { + "path": { "type": "string", - "description": "Glob pattern to match, e.g. *.ts or src/**/*.ts" + "description": "Relative path from the project root, e.g. src/main.ts" } }, "required": [ - "pattern" + "path" ] - }, - "importPath": "fs/glob/index.ts" + } }, { - "name": "fs/write", + "name": "fs.write", "description": "Write content to a file. The path must be relative to the project root, e.g. src/main.ts or README.md. Never use absolute paths or placeholder paths like /path/to/file.", - "entry": "index.ts", - "parameters": { + "importPath": "fs/write/index.ts", + "inputSchema": { "type": "object", "properties": { "path": { @@ -94,14 +80,30 @@ "path", "content" ] - }, - "importPath": "fs/write/index.ts" + } + }, + { + "name": "github.closes_issue", + "description": "Closes an existing issue in a GitHub repository.", + "importPath": "github/close_issue/index.ts", + "inputSchema": { + "type": "object", + "properties": { + "issue_number": { + "type": "number", + "description": "The issue number to close" + } + }, + "required": [ + "issue_number" + ] + } }, { - "name": "github/comment_issue", + "name": "github.comment_issue", "description": "Adds a comment to an existing GitHub issue.", - "entry": "./index.ts", - "parameters": { + "importPath": "github/comment_issue/index.ts", + "inputSchema": { "type": "object", "properties": { "issue_number": { @@ -117,14 +119,38 @@ "issue_number", "body" ] - }, - "importPath": "github/comment_issue/index.ts" + } }, { - "name": "github/search_issues", + "name": "github.create_issue", + "description": "Files a new issue in a GitHub repository.", + "importPath": "github/create_issue/index.ts", + "inputSchema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "body": { + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "title" + ] + } + }, + { + "name": "github.search_issues", "description": "Search for GitHub issues in the repository.", - "entry": "./index.ts", - "parameters": { + "importPath": "github/search_issues/index.ts", + "inputSchema": { "type": "object", "properties": { "query": { @@ -144,47 +170,23 @@ "required": [ "query" ] - }, - "importPath": "github/search_issues/index.ts" - }, - { - "name": "github/get_repo_info", - "description": "Get the GitHub repository (owner and name) that ticket operations will target. Use this to discover which repository the agent is configured to work with.", - "entry": "index.ts", - "parameters": { - "type": "object", - "properties": {}, - "required": [] - }, - "importPath": "github/get_repo_info/index.ts" + } }, { - "name": "github/create_issue", - "description": "Create a new GitHub issue in the configured repository. REQUIRED: title. OPTIONAL: body (description), labels (tags for the issue).", - "entry": "./index.ts", - "parameters": { + "name": "smoke", + "description": "A sanity check tool to verify tool-calling is working. Triggered when the user asks what day it is.", + "importPath": "smoke/index.ts", + "inputSchema": { "type": "object", "properties": { - "title": { - "type": "string", - "description": "Brief title for the issue (required). Example: 'Fix login button styling' or 'Add email validation'" - }, - "body": { + "query": { "type": "string", - "description": "Detailed description of the issue (optional). Include context, steps to reproduce, or desired behavior." - }, - "labels": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Tags to categorize the issue (optional). Examples: 'bug', 'feature', 'docs', 'enhancement'" + "description": "The user's question about the date or time" } }, "required": [ - "title" + "query" ] - }, - "importPath": "github/create_issue/index.ts" + } } -] \ No newline at end of file +] diff --git a/packages/tools/registry.json.bak b/packages/tools/registry.json.bak new file mode 100644 index 00000000..487aa793 --- /dev/null +++ b/packages/tools/registry.json.bak @@ -0,0 +1,173 @@ +[ + { + "name": "smoke", + "description": "A sanity check tool to verify tool-calling is working. Triggered when the user asks what day it is.", + "entry": "index.ts", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The user's question about the date or time" + } + }, + "required": [ + "query" + ] + }, + "importPath": "smoke/index.ts" + }, + { + "name": "github/create_issue", + "description": "Files a new issue in a GitHub repository.", + "entry": "./index.ts", + "parameters": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "body": { + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "title" + ] + }, + "importPath": "github/create_issue/index.ts" + }, + { + "name": "github/search_issues", + "description": "Search for GitHub issues in the repository.", + "entry": "./index.ts", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query" + }, + "state": { + "type": "string", + "enum": [ + "open", + "closed", + "all" + ], + "description": "Issue state filter" + } + }, + "required": [ + "query" + ] + }, + "importPath": "github/search_issues/index.ts" + }, + { + "name": "github/comment_issue", + "description": "Adds a comment to an existing GitHub issue.", + "entry": "./index.ts", + "parameters": { + "type": "object", + "properties": { + "issue_number": { + "type": "number", + "description": "The issue number to comment on" + }, + "body": { + "type": "string", + "description": "The comment text" + } + }, + "required": [ + "issue_number", + "body" + ] + }, + "importPath": "github/comment_issue/index.ts" + }, + { + "name": "fs/write", + "description": "Write content to a file. The path must be relative to the project root, e.g. src/main.ts or README.md. Never use absolute paths or placeholder paths like /path/to/file.", + "entry": "index.ts", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Relative path from the project root, e.g. src/main.ts or README.md" + }, + "content": { + "type": "string", + "description": "The full content to write to the file" + } + }, + "required": [ + "path", + "content" + ] + }, + "importPath": "fs/write/index.ts" + }, + { + "name": "fs/read", + "description": "Read the content of a file. The path must be relative to the project root, e.g. src/main.ts or README.md.", + "entry": "index.ts", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Relative path from the project root, e.g. src/main.ts" + } + }, + "required": [ + "path" + ] + }, + "importPath": "fs/read/index.ts" + }, + { + "name": "fs/bash", + "description": "Run a shell command in the project root directory.", + "entry": "index.ts", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The shell command to run, e.g. npm test or git status" + } + }, + "required": [ + "command" + ] + }, + "importPath": "fs/bash/index.ts" + }, + { + "name": "fs/glob", + "description": "List files in the project matching a pattern, e.g. *.ts or *.json.", + "entry": "index.ts", + "parameters": { + "type": "object", + "properties": { + "pattern": { + "type": "string", + "description": "Glob pattern to match, e.g. *.ts or src/**/*.ts" + } + }, + "required": [ + "pattern" + ] + }, + "importPath": "fs/glob/index.ts" + } +] \ No newline at end of file diff --git a/packages/tools/smoke/index.ts b/packages/tools/smoke/index.ts index f9a8d67f..966b2c70 100644 --- a/packages/tools/smoke/index.ts +++ b/packages/tools/smoke/index.ts @@ -1,4 +1,5 @@ /** + * @file packages/tools/smoke/index.ts * Smoke test tool to verify registry and execution flow. */ export async function run(params: { query: string }): Promise { diff --git a/scripts/generate-tool-registry.ts b/scripts/generate-tool-registry.ts index c739238c..4ac2373f 100644 --- a/scripts/generate-tool-registry.ts +++ b/scripts/generate-tool-registry.ts @@ -1,54 +1,151 @@ -import { readdir, writeFile } from "node:fs/promises"; -import path from "node:path"; - -const TOOLS_ROOT = path.join(process.cwd(), "packages/tools"); -const OUTPUT_FILE = path.join(TOOLS_ROOT, "registry.json"); - -async function generate() { - const tools: any[] = []; - - // We look specifically for gh-tools or any subfolders in packages/tools - // This supports your nested github/create_issue structure - async function scan(dir: string) { - const entries = await readdir(dir, { withFileTypes: true }); - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - - if (entry.isDirectory()) { - if (entry.name === "node_modules") continue; - - // If we find a manifest, this is a tool - try { - const manifestPath = path.join(fullPath, "manifest.json"); - const manifestFile = Bun.file(manifestPath); - - if (await manifestFile.exists()) { - const content = await manifestFile.json(); - tools.push({ - ...content, - // Relative path for the runtime loader to use - importPath: path.relative(TOOLS_ROOT, path.join(fullPath, content.entry || "index.ts")) - // Even better: - // importPath: path.relative(TOOLS_ROOT, path.join(fullPath, content.entry || "index.ts")).replace(/\\/g, '/') - - - }); - continue; // Found a tool, no need to go deeper into this specific folder - } - } catch (e) { - // No manifest here, keep digging - } - - await scan(fullPath); +/** + * @file scripts/generate-tool-registry.ts + * @description Scans tool manifests, validates MCP tool names, skips broken manifests, + * and updates registry.json with all valid tools. + */ +import { readdir, readFile, writeFile } from "node:fs/promises"; +import { join, relative } from "node:path"; + +const TOOLS_DIR = join(import.meta.dir, "../packages/tools"); +const REGISTRY_PATH = join(TOOLS_DIR, "registry.json"); +const MCP_TOOL_NAME_REGEX = /^[A-Za-z0-9_.-]+$/; + +interface ToolManifest { + name: string; + description?: string; + entry?: string; + inputSchema?: Record; + parameters?: Record; +} + +interface RegistryEntry { + name: string; + description: string; + importPath: string; + inputSchema: Record; +} + +interface ManifestError { + manifestPath: string; + reason: string; + suggestion?: string; +} + +async function findManifests(dir: string): Promise { + const entries = await readdir(dir, { recursive: true, withFileTypes: true }); + return entries + .filter((e) => e.isFile() && e.name === "manifest.json") + .map((e) => join(e.parentPath, e.name)); +} + +async function generateRegistry() { + const manifestPaths = await findManifests(TOOLS_DIR); + const totalManifests = manifestPaths.length; + const registry: RegistryEntry[] = []; + const errors: ManifestError[] = []; + + for (const manifestPath of manifestPaths) { + const relPath = relative(TOOLS_DIR, manifestPath); + let raw = ""; + + try { + raw = await readFile(manifestPath, "utf-8"); + } catch (err) { + errors.push({ + manifestPath: relPath, + reason: `Could not read file: ${err instanceof Error ? err.message : String(err)}` + }); + continue; + } + + if (!raw.trim()) { + errors.push({ + manifestPath: relPath, + reason: "File is completely empty (Unexpected EOF)" + }); + continue; + } + + let manifest: ToolManifest; + try { + manifest = JSON.parse(raw); + } catch (err) { + errors.push({ + manifestPath: relPath, + reason: `Invalid JSON syntax: ${err instanceof Error ? err.message : String(err)}` + }); + continue; + } + + if (!manifest.name) { + errors.push({ + manifestPath: relPath, + reason: "Missing required property 'name' in manifest.json" + }); + continue; + } + + if (!MCP_TOOL_NAME_REGEX.test(manifest.name)) { + errors.push({ + manifestPath: relPath, + reason: `Invalid MCP tool name "${manifest.name}". Must match regex ${MCP_TOOL_NAME_REGEX}`, + suggestion: `Change "${manifest.name}" to "${manifest.name.replaceAll("/", ".")}"` + }); + continue; + } + + const toolDir = relative(TOOLS_DIR, join(manifestPath, "..")); + const importPath = join(toolDir, manifest.entry || "index.ts"); + + registry.push({ + name: manifest.name, + description: manifest.description || "", + importPath, + inputSchema: manifest.inputSchema || manifest.parameters || {} + }); + } + + // Report errors for skipped manifests + if (errors.length > 0) { + console.error(`\n⚠️ Skipped ${errors.length} invalid tool manifest(s):\n`); + for (const err of errors) { + console.error(` • ${err.manifestPath}`); + console.error(` Error: ${err.reason}`); + if (err.suggestion) { + console.error(` Fix: ${err.suggestion}`); } + console.error(""); } } - await scan(TOOLS_ROOT); - - await writeFile(OUTPUT_FILE, JSON.stringify(tools, null, 2)); - console.log(`🚀 Registry generated: ${tools.length} tools found.`); + // Only hard fail if zero tools could be loaded at all + if (registry.length === 0) { + console.error(`❌ Critical failure: 0 tools were successfully loaded out of ${totalManifests} found.`); + process.exit(1); + } + + // Sort deterministically + registry.sort((a, b) => a.name.localeCompare(b.name)); + + const newContent = JSON.stringify(registry, null, 2) + "\n"; + + let existingContent = ""; + try { + existingContent = await readFile(REGISTRY_PATH, "utf-8"); + } catch { + // registry.json does not exist yet + } + + if (existingContent === newContent) { + console.log(`ℹ️ Registry unchanged (${registry.length} tools out of ${totalManifests} loaded). [no-op]`); + return; + } + + await writeFile(REGISTRY_PATH, newContent, "utf-8"); + console.log(`🚀 Registry updated: ${registry.length} tools out of ${totalManifests} loaded -> packages/tools/registry.json`); } -generate().catch(console.error); \ No newline at end of file +generateRegistry().catch((err) => { + console.error("❌ Fatal execution error:", err); + process.exit(1); +}); \ No newline at end of file diff --git a/scripts/generate-tool-registry.ts.bak b/scripts/generate-tool-registry.ts.bak new file mode 100644 index 00000000..d1fc9218 --- /dev/null +++ b/scripts/generate-tool-registry.ts.bak @@ -0,0 +1,58 @@ +// scripts/generate-tool-registry.ts +import { readdir, writeFile } from "node:fs/promises"; +import path from "node:path"; + +const TOOLS_ROOT = path.join(process.cwd(), "packages/tools"); +const OUTPUT_FILE = path.join(TOOLS_ROOT, "registry.json"); + +async function generate() { + const tools: any[] = []; + + async function scan(dir: string) { + const entries = await readdir(dir, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + if (entry.name === "node_modules") continue; + + try { + const manifestPath = path.join(fullPath, "manifest.json"); + const manifestFile = Bun.file(manifestPath); + + if (await manifestFile.exists()) { + const content = await manifestFile.json(); + + // Map parameters/input_schema to standard MCP inputSchema + const schema = content.inputSchema || content.parameters || content.input_schema || { type: "object", properties: {} }; + + // Clean up old non-standard keys so registry stays strict + delete content.parameters; + delete content.input_schema; + + const relativePath = path.relative(TOOLS_ROOT, path.join(fullPath, content.entry || "index.ts")); + + tools.push({ + ...content, + inputSchema: schema, + importPath: relativePath.replace(/\\/g, '/') + }); + continue; + } + } catch (e) { + // No manifest here, keep digging + } + + await scan(fullPath); + } + } + } + + await scan(TOOLS_ROOT); + + await writeFile(OUTPUT_FILE, JSON.stringify(tools, null, 2)); + console.log(`🚀 Registry generated: ${tools.length} tools found.`); +} + +generate().catch(console.error); \ No newline at end of file