From e3296f3e1fe82bc27c538a97d4a8731cfacaab1a Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 6 Mar 2026 14:57:09 +0100 Subject: [PATCH 1/4] refactor: add enclave mcp package --- package.json | 6 +- packages/enclave-mcp/README.md | 89 ++++++++++++ packages/enclave-mcp/package.json | 36 +++++ packages/enclave-mcp/src/index.ts | 166 ++++++++++++++++++++++ packages/enclave-mcp/tsconfig.json | 12 ++ packages/enclave-mcp/tsup.config.ts | 10 ++ pnpm-lock.yaml | 207 ++++++++++++++++++++++++++++ pnpm-workspace.yaml | 1 + scripts/bump-versions.ts | 1 + 9 files changed, 526 insertions(+), 2 deletions(-) create mode 100644 packages/enclave-mcp/README.md create mode 100644 packages/enclave-mcp/package.json create mode 100644 packages/enclave-mcp/src/index.ts create mode 100644 packages/enclave-mcp/tsconfig.json create mode 100644 packages/enclave-mcp/tsup.config.ts diff --git a/package.json b/package.json index 95f67d6ac4..ea36a7554a 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "evm:clean": "cd packages/enclave-contracts && pnpm clean:deployments", "evm:coverage": "cd packages/enclave-contracts && pnpm coverage", "evm:release": "cd packages/enclave-contracts && pnpm release", + "mcp:build": "cd packages/enclave-mcp && pnpm build", + "mcp:release": "cd packages/enclave-mcp && pnpm release", "react:build": "cd packages/enclave-react && pnpm build", "sdk:build": "cd packages/enclave-sdk && pnpm build", "sdk:test": "cd packages/enclave-sdk && pnpm test", @@ -59,11 +61,11 @@ "wasm:release": "cd crates/wasm && pnpm release", "config:release": "cd packages/enclave-config && pnpm release", "react:release": "cd packages/enclave-react && pnpm release", - "npm:release": "pnpm build && pnpm config:release && pnpm evm:release && pnpm wasm:release && pnpm sdk:release && pnpm react:release", + "npm:release": "pnpm build && pnpm config:release && pnpm evm:release && pnpm wasm:release && pnpm sdk:release && pnpm react:release && pnpm mcp:release", "support:build": "cd crates/support && ./scripts/build.sh", "build": "pnpm compile", "wasm:build": "cd ./crates/wasm && pnpm build", - "build:ts": "pnpm evm:build && pnpm sdk:build && pnpm react:build", + "build:ts": "pnpm evm:build && pnpm sdk:build && pnpm react:build && pnpm mcp:build", "template:build": "cd templates/default && pnpm compile", "prepare-publish": "tsx .github/scripts/prepareForNpmPublishing.ts" }, diff --git a/packages/enclave-mcp/README.md b/packages/enclave-mcp/README.md new file mode 100644 index 0000000000..8c69619561 --- /dev/null +++ b/packages/enclave-mcp/README.md @@ -0,0 +1,89 @@ +# @enclave-e3/mcp + +MCP server for [Enclave](https://enclave.gg) documentation. Allows AI assistants to answer questions about Enclave by fetching content directly from [docs.enclave.gg](https://docs.enclave.gg). + +## Tools + +| Tool | Description | +|------|-------------| +| `list_docs` | List all available documentation pages | +| `read_doc` | Read a specific page by slug (e.g. `introduction`, `ciphernode-operators/running`) | +| `search_docs` | Search for a keyword across all pages | + +## Integration + +### Claude Desktop + +Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows): + +```json +{ + "mcpServers": { + "enclave-docs": { + "command": "npx", + "args": ["-y", "@enclave-e3/mcp"] + } + } +} +``` + +Restart Claude Desktop. The tools will be available automatically. + +### VS Code (Continue) + +Add a file `.continue/mcpServers/enclave.yaml` in your project: + +```yaml +name: Enclave Docs +version: 0.1.0 +schema: v1 +mcpServers: + - name: enclave-docs + command: npx + args: + - -y + - "@enclave-e3/mcp" +``` + +### Cursor + +Edit `~/.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "enclave-docs": { + "command": "npx", + "args": ["-y", "@enclave-e3/mcp"] + } + } +} +``` + +### Windsurf + +Edit `~/.codeium/windsurf/mcp_config.json`: + +```json +{ + "mcpServers": { + "enclave-docs": { + "command": "npx", + "args": ["-y", "@enclave-e3/mcp"] + } + } +} +``` + +## Usage + +Once configured, ask your AI assistant questions like: + +- *"What is an E3 in Enclave?"* +- *"How do I run a ciphernode?"* +- *"Explain the Enclave architecture"* +- *"Search the enclave docs for threshold encryption"* + +## License + +LGPL-3.0-only \ No newline at end of file diff --git a/packages/enclave-mcp/package.json b/packages/enclave-mcp/package.json new file mode 100644 index 0000000000..5930c27d92 --- /dev/null +++ b/packages/enclave-mcp/package.json @@ -0,0 +1,36 @@ +{ + "name": "@enclave-e3/mcp", + "version": "0.1.0", + "description": "MCP server for Enclave documentation", + "type": "module", + "bin": { + "enclave-mcp": "./dist/index.js" + }, + "files": [ + "dist" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/gnosisguild/enclave.git", + "directory": "packages/enclave-mcp" + }, + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "prerelease": "pnpm build", + "release": "pnpm publish --access=public" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.10.2", + "node-html-parser": "^7.0.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "tsup": "^8.5.0", + "typescript": "5.8.3" + }, + "license": "LGPL-3.0-only" +} diff --git a/packages/enclave-mcp/src/index.ts b/packages/enclave-mcp/src/index.ts new file mode 100644 index 0000000000..bbbcce7fee --- /dev/null +++ b/packages/enclave-mcp/src/index.ts @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { parse } from 'node-html-parser' +import { z } from 'zod' +import pkg from '../package.json' with { type: 'json' } + +const { version } = pkg + +const BASE_URL = 'https://docs.enclave.gg' + +interface DocPage { + slug: string + title: string + url: string +} + +const DOC_PAGES: DocPage[] = [ + { slug: 'introduction', title: 'Introduction', url: '/introduction' }, + { slug: 'what-is-e3', title: 'What is an E3?', url: '/what-is-e3' }, + { slug: 'architecture-overview', title: 'Architecture Overview', url: '/architecture-overview' }, + { slug: 'computation-flow', title: 'E3 Computation Flow', url: '/computation-flow' }, + { slug: 'use-cases', title: 'Use Cases', url: '/use-cases' }, + { slug: 'building-with-enclave', title: 'Building with Enclave', url: '/building-with-enclave' }, + { slug: 'best-practices', title: 'Best Practices', url: '/best-practices' }, + { slug: 'installation', title: 'Installation', url: '/installation' }, + { slug: 'quick-start', title: 'Quick Start', url: '/quick-start' }, + { slug: 'hello-world-tutorial', title: 'Hello World Tutorial', url: '/hello-world-tutorial' }, + { slug: 'project-template', title: 'Project Template', url: '/project-template' }, + { slug: 'sdk', title: 'Enclave SDK', url: '/sdk' }, + { slug: 'setting-up-server', title: 'Setting Up the Server', url: '/setting-up-server' }, + { slug: 'noir-circuits', title: 'Noir Circuits', url: '/noir-circuits' }, + { slug: 'getting-started', title: 'Getting Started (Build an E3)', url: '/getting-started' }, + { slug: 'write-secure-program', title: 'Writing the Secure Process', url: '/write-secure-program' }, + { slug: 'write-e3-contract', title: 'Writing the E3 Program Contract', url: '/write-e3-contract' }, + { slug: 'compute-provider', title: 'Compute Provider Setup', url: '/compute-provider' }, + { slug: 'putting-it-together', title: 'Putting It All Together', url: '/putting-it-together' }, + { slug: 'whitepaper', title: 'White Paper', url: '/whitepaper' }, + { slug: 'ciphernode-operators', title: 'Ciphernode Operators Overview', url: '/ciphernode-operators' }, + { slug: 'ciphernode-operators/running', title: 'Running a Ciphernode', url: '/ciphernode-operators/running' }, + { slug: 'ciphernode-operators/registration', title: 'Registration & Licensing', url: '/ciphernode-operators/registration' }, + { slug: 'ciphernode-operators/tickets-and-sortition', title: 'Tickets & Sortition', url: '/ciphernode-operators/tickets-and-sortition' }, + { slug: 'ciphernode-operators/exits-and-slashing', title: 'Exits, Rewards & Slashing', url: '/ciphernode-operators/exits-and-slashing' }, + { slug: 'CRISP/introduction', title: 'CRISP Introduction', url: '/CRISP/introduction' }, + { slug: 'CRISP/setup', title: 'CRISP Setup', url: '/CRISP/setup' }, + { slug: 'CRISP/running-e3', title: 'CRISP Running an E3 Program', url: '/CRISP/running-e3' }, +] + +async function fetchDocPage(url: string): Promise { + const fullUrl = `${BASE_URL}${url}` + const response = await fetch(fullUrl) + if (!response.ok) { + throw new Error(`Failed to fetch ${fullUrl}: ${response.status} ${response.statusText}`) + } + const html = await response.text() + const root = parse(html) + + // Remove nav, header, footer, scripts, styles + root.querySelectorAll("nav, header, footer, script, style, [aria-hidden='true']").forEach((el) => el.remove()) + + // Try to get the main article content + const article = root.querySelector('article') ?? root.querySelector('main') ?? root.querySelector('.nextra-content') + const content = article ?? root + + return content.text.replace(/\n{3,}/g, '\n\n').trim() +} + +const server = new McpServer({ + name: 'enclave-docs', + version, +}) + +// Resource: list all doc pages +server.registerResource('docs-index', 'docs://index', { description: 'Index of all Enclave documentation pages' }, async () => ({ + contents: [ + { + uri: 'docs://index', + text: DOC_PAGES.map((p) => `- [${p.title}](docs://${p.slug})`).join('\n'), + mimeType: 'text/markdown', + }, + ], +})) + +// Resource: individual doc pages +for (const page of DOC_PAGES) { + server.registerResource(page.slug, `docs://${page.slug}`, { description: page.title }, async () => { + const content = await fetchDocPage(page.url) + return { + contents: [{ uri: `docs://${page.slug}`, text: content, mimeType: 'text/plain' }], + } + }) +} + +// Tool: read a specific doc page +server.registerTool( + 'read_doc', + { + description: 'Fetch and read a specific Enclave documentation page by slug', + inputSchema: z.object({ slug: z.string().describe("Page slug, e.g. 'introduction', 'ciphernode-operators/running'") }), + }, + async ({ slug }) => { + const page = DOC_PAGES.find((p) => p.slug === slug) + if (!page) { + const available = DOC_PAGES.map((p) => p.slug).join(', ') + return { content: [{ type: 'text', text: `Page "${slug}" not found. Available: ${available}` }], isError: true } + } + const content = await fetchDocPage(page.url) + return { content: [{ type: 'text', text: `# ${page.title}\n\n${content}` }] } + }, +) + +// Tool: search across all docs +server.registerTool( + 'search_docs', + { + description: 'Search for a keyword or phrase across all Enclave documentation pages', + inputSchema: z.object({ query: z.string().describe('Search query') }), + }, + async ({ query }) => { + const lower = query.toLowerCase() + const results: string[] = [] + + await Promise.all( + DOC_PAGES.map(async (page) => { + try { + const content = await fetchDocPage(page.url) + if (content.toLowerCase().includes(lower)) { + const idx = content.toLowerCase().indexOf(lower) + const start = Math.max(0, idx - 150) + const end = Math.min(content.length, idx + 300) + const snippet = content.slice(start, end).replace(/\n+/g, ' ').trim() + results.push(`## ${page.title}\nURL: ${BASE_URL}${page.url}\n\n...${snippet}...`) + } + } catch { + // skip pages that fail to load + } + }), + ) + + if (results.length === 0) { + return { content: [{ type: 'text', text: `No results found for "${query}".` }] } + } + + return { + content: [ + { + type: 'text', + text: `Found ${results.length} page(s) matching "${query}":\n\n${results.join('\n\n---\n\n')}`, + }, + ], + } + }, +) + +// Tool: list all available doc pages +server.registerTool('list_docs', { description: 'List all available Enclave documentation pages' }, async () => { + const list = DOC_PAGES.map((p) => `- **${p.title}** → slug: \`${p.slug}\``).join('\n') + return { content: [{ type: 'text', text: `# Enclave Documentation Pages\n\n${list}` }] } +}) + +const transport = new StdioServerTransport() +await server.connect(transport) diff --git a/packages/enclave-mcp/tsconfig.json b/packages/enclave-mcp/tsconfig.json new file mode 100644 index 0000000000..4230738bba --- /dev/null +++ b/packages/enclave-mcp/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "outDir": "dist" + }, + "include": ["src"] +} diff --git a/packages/enclave-mcp/tsup.config.ts b/packages/enclave-mcp/tsup.config.ts new file mode 100644 index 0000000000..8c442f0564 --- /dev/null +++ b/packages/enclave-mcp/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + dts: false, + banner: { + js: "#!/usr/bin/env node", + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d7669afc7..8d3e39dbe7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -573,6 +573,25 @@ importers: specifier: 5.8.3 version: 5.8.3 + packages/enclave-mcp: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.10.2 + version: 1.27.1(zod@3.25.76) + node-html-parser: + specifier: ^7.0.1 + version: 7.1.0 + zod: + specifier: ^3.23.8 + version: 3.25.76 + devDependencies: + tsup: + specifier: 8.5.0 + version: 8.5.0(@microsoft/api-extractor@7.54.0(@types/node@22.7.5))(@swc/core@1.15.0(@swc/helpers@0.5.17))(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.8.3)(yaml@2.8.2) + typescript: + specifier: 5.8.3 + version: 5.8.3 + packages/enclave-react: dependencies: '@enclave-e3/sdk': @@ -2192,6 +2211,12 @@ packages: react: ^16 || ^17 || ^18 react-dom: ^16 || ^17 || ^18 + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2480,6 +2505,16 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@modelcontextprotocol/sdk@1.27.1': + resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@motionone/animation@10.18.0': resolution: {integrity: sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==} @@ -4709,6 +4744,10 @@ packages: resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} engines: {node: '>=18'} + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -5169,6 +5208,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -5916,6 +5959,14 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + evp_bytestokey@1.0.3: resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} @@ -5931,10 +5982,20 @@ packages: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} + express-rate-limit@8.3.0: + resolution: {integrity: sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@5.1.0: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -6514,6 +6575,10 @@ packages: resolution: {integrity: sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg==} engines: {node: '>=16.9.0'} + hono@4.12.5: + resolution: {integrity: sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==} + engines: {node: '>=16.9.0'} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -6647,6 +6712,10 @@ packages: resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019. + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -6857,6 +6926,9 @@ packages: jose@6.1.0: resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==} + jose@6.2.0: + resolution: {integrity: sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -6907,6 +6979,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -7685,6 +7760,9 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true + node-html-parser@7.1.0: + resolution: {integrity: sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==} + node-mock-http@1.0.3: resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==} @@ -8027,6 +8105,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -8398,6 +8480,10 @@ packages: resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} engines: {node: '>=0.6'} + qs@6.15.0: + resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + engines: {node: '>=0.6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -10095,6 +10181,11 @@ packages: peerDependencies: ethers: ~5.7.0 + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + zod-validation-error@4.0.2: resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} engines: {node: '>=18.0.0'} @@ -11843,6 +11934,10 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@hono/node-server@1.19.11(hono@4.12.5)': + dependencies: + hono: 4.12.5 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -12242,6 +12337,28 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} + '@modelcontextprotocol/sdk@1.27.1(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.11(hono@4.12.5) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 8.3.0(express@5.2.1) + hono: 4.12.5 + jose: 6.2.0 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.1 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@motionone/animation@10.18.0': dependencies: '@motionone/easing': 10.18.0 @@ -15467,6 +15584,10 @@ snapshots: optionalDependencies: ajv: 8.13.0 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -15767,6 +15888,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3(supports-color@8.1.1) + http-errors: 2.0.0 + iconv-lite: 0.7.0 + on-finished: 2.4.1 + qs: 6.15.0 + raw-body: 3.0.1 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} borsh@0.7.0: @@ -16277,6 +16412,11 @@ snapshots: core-util-is@1.0.3: {} + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + cose-base@1.0.3: dependencies: layout-base: 1.0.2 @@ -17232,6 +17372,12 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + evp_bytestokey@1.0.3: dependencies: md5.js: 1.3.5 @@ -17271,6 +17417,11 @@ snapshots: signal-exit: 4.1.0 strip-final-newline: 3.0.0 + express-rate-limit@8.3.0(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.1.0 + express@5.1.0: dependencies: accepts: 2.0.0 @@ -17303,6 +17454,39 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3(supports-color@8.1.1) + depd: 2.0.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.1 + 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.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + exsolve@1.0.7: {} extend-shallow@2.0.1: @@ -18125,6 +18309,8 @@ snapshots: hono@4.10.4: {} + hono@4.12.5: {} + html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} @@ -18237,6 +18423,8 @@ snapshots: intersection-observer@0.12.2: {} + ip-address@10.1.0: {} + ipaddr.js@1.9.1: {} iron-webcrypto@1.2.1: {} @@ -18426,6 +18614,8 @@ snapshots: jose@6.1.0: {} + jose@6.2.0: {} + joycon@3.1.1: {} js-sha3@0.8.0: {} @@ -18462,6 +18652,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-schema-typed@8.0.2: {} + json-stable-stringify-without-jsonify@1.0.1: {} json-stream-stringify@3.1.6: {} @@ -19735,6 +19927,11 @@ snapshots: node-gyp-build@4.8.4: {} + node-html-parser@7.1.0: + dependencies: + css-select: 5.2.2 + he: 1.2.0 + node-mock-http@1.0.3: {} node-releases@2.0.27: {} @@ -20160,6 +20357,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-dir@4.2.0: dependencies: find-up: 4.1.0 @@ -20416,6 +20615,10 @@ snapshots: dependencies: side-channel: 1.1.0 + qs@6.15.0: + dependencies: + side-channel: 1.1.0 + quansync@0.2.11: {} query-string@7.1.3: @@ -22494,6 +22697,10 @@ snapshots: dependencies: ethers: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-validation-error@4.0.2(zod@4.1.12): dependencies: zod: 4.1.12 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 296a88e078..1808db834a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -10,6 +10,7 @@ packages: - packages/enclave-react - packages/enclave-sdk - packages/enclave-contracts + - packages/enclave-mcp - templates/default - templates/default/client linkWorkspacePackages: true diff --git a/scripts/bump-versions.ts b/scripts/bump-versions.ts index ece90f3bb4..55cc1dfc1f 100644 --- a/scripts/bump-versions.ts +++ b/scripts/bump-versions.ts @@ -387,6 +387,7 @@ class VersionBumper { 'packages/enclave-contracts', 'packages/enclave-config', 'packages/enclave-react', + 'packages/enclave-mcp', 'crates/wasm', ] From c5991492b7725198faefad9df1de3cb71f3caca2 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 6 Mar 2026 14:59:38 +0100 Subject: [PATCH 2/4] fix: eslint error --- scripts/build-circuits.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-circuits.ts b/scripts/build-circuits.ts index 09fa4b2f78..39c3017485 100644 --- a/scripts/build-circuits.ts +++ b/scripts/build-circuits.ts @@ -9,7 +9,7 @@ import { execSync } from 'child_process' import { createHash } from 'crypto' import { appendFileSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs' import { basename, join, resolve } from 'path' -import { ALL_GROUPS, CIRCUIT_GROUPS, CIRCUIT_VARIANTS, type CircuitGroup, type CircuitVariant } from './circuit-constants' +import { ALL_GROUPS, CIRCUIT_GROUPS, CIRCUIT_VARIANTS, type CircuitGroup } from './circuit-constants' interface CircuitInfo { name: string From b19e52de8165e364c95d32b5eb723cedd1a0ef88 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 6 Mar 2026 15:01:02 +0100 Subject: [PATCH 3/4] chore: add license header --- packages/enclave-mcp/tsup.config.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/enclave-mcp/tsup.config.ts b/packages/enclave-mcp/tsup.config.ts index 8c442f0564..dca02e7fd8 100644 --- a/packages/enclave-mcp/tsup.config.ts +++ b/packages/enclave-mcp/tsup.config.ts @@ -1,3 +1,9 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + import { defineConfig } from "tsup"; export default defineConfig({ From 26b86283fef297e5b2caf9f5c4de24acd4522fe5 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Fri, 6 Mar 2026 16:00:10 +0100 Subject: [PATCH 4/4] refactor: apply coderabbit suggestions --- packages/enclave-mcp/README.md | 4 +++ packages/enclave-mcp/package.json | 3 ++ packages/enclave-mcp/src/index.ts | 60 ++++++++++++++++++++++++++++--- 3 files changed, 62 insertions(+), 5 deletions(-) diff --git a/packages/enclave-mcp/README.md b/packages/enclave-mcp/README.md index 8c69619561..74c39e5ee8 100644 --- a/packages/enclave-mcp/README.md +++ b/packages/enclave-mcp/README.md @@ -2,6 +2,10 @@ MCP server for [Enclave](https://enclave.gg) documentation. Allows AI assistants to answer questions about Enclave by fetching content directly from [docs.enclave.gg](https://docs.enclave.gg). +## Requirements + +- Node.js **>=18.20.0** — required for ESM JSON import attributes, global `fetch`, and top-level await used by the `enclave-mcp` CLI. + ## Tools | Tool | Description | diff --git a/packages/enclave-mcp/package.json b/packages/enclave-mcp/package.json index 5930c27d92..70aa2bf104 100644 --- a/packages/enclave-mcp/package.json +++ b/packages/enclave-mcp/package.json @@ -32,5 +32,8 @@ "tsup": "^8.5.0", "typescript": "5.8.3" }, + "engines": { + "node": ">=18.20.0" + }, "license": "LGPL-3.0-only" } diff --git a/packages/enclave-mcp/src/index.ts b/packages/enclave-mcp/src/index.ts index bbbcce7fee..beaef3dab1 100644 --- a/packages/enclave-mcp/src/index.ts +++ b/packages/enclave-mcp/src/index.ts @@ -12,6 +12,7 @@ import pkg from '../package.json' with { type: 'json' } const { version } = pkg const BASE_URL = 'https://docs.enclave.gg' +const FETCH_TIMEOUT_MS = 10_000 interface DocPage { slug: string @@ -19,7 +20,8 @@ interface DocPage { url: string } -const DOC_PAGES: DocPage[] = [ +// Fallback corpus used when the sitemap cannot be fetched. +const STATIC_DOC_PAGES: DocPage[] = [ { slug: 'introduction', title: 'Introduction', url: '/introduction' }, { slug: 'what-is-e3', title: 'What is an E3?', url: '/what-is-e3' }, { slug: 'architecture-overview', title: 'Architecture Overview', url: '/architecture-overview' }, @@ -50,9 +52,44 @@ const DOC_PAGES: DocPage[] = [ { slug: 'CRISP/running-e3', title: 'CRISP Running an E3 Program', url: '/CRISP/running-e3' }, ] +function fetchWithTimeout(url: string): Promise { + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS) + return fetch(url, { signal: controller.signal }).finally(() => clearTimeout(timer)) +} + +// Attempt to build the page corpus from the sitemap so it stays current +// without manual updates. Falls back to STATIC_DOC_PAGES on any failure. +async function loadDocPages(): Promise { + try { + const response = await fetchWithTimeout(`${BASE_URL}/sitemap.xml`) + if (!response.ok) return STATIC_DOC_PAGES + const xml = await response.text() + const root = parse(xml) + const locs = root.querySelectorAll('loc').map((el) => el.text.trim()) + if (locs.length === 0) return STATIC_DOC_PAGES + return locs + .filter((loc) => loc.startsWith(BASE_URL)) + .map((loc) => { + const path = loc.slice(BASE_URL.length) || '/' + const slug = path.replace(/^\//, '') + const known = STATIC_DOC_PAGES.find((p) => p.slug === slug) + const title = + known?.title ?? + slug + .split('/') + .map((s) => s.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())) + .join(' / ') + return { slug, title, url: path } + }) + } catch { + return STATIC_DOC_PAGES + } +} + async function fetchDocPage(url: string): Promise { const fullUrl = `${BASE_URL}${url}` - const response = await fetch(fullUrl) + const response = await fetchWithTimeout(fullUrl) if (!response.ok) { throw new Error(`Failed to fetch ${fullUrl}: ${response.status} ${response.statusText}`) } @@ -69,6 +106,8 @@ async function fetchDocPage(url: string): Promise { return content.text.replace(/\n{3,}/g, '\n\n').trim() } +const DOC_PAGES = await loadDocPages() + const server = new McpServer({ name: 'enclave-docs', version, @@ -121,8 +160,13 @@ server.registerTool( inputSchema: z.object({ query: z.string().describe('Search query') }), }, async ({ query }) => { + if (!query.trim()) { + return { content: [{ type: 'text', text: 'Query must not be empty.' }], isError: true } + } + const lower = query.toLowerCase() const results: string[] = [] + const failures: string[] = [] await Promise.all( DOC_PAGES.map(async (page) => { @@ -136,20 +180,26 @@ server.registerTool( results.push(`## ${page.title}\nURL: ${BASE_URL}${page.url}\n\n...${snippet}...`) } } catch { - // skip pages that fail to load + failures.push(`${page.title} (${page.url})`) } }), ) + const failureSummary = failures.length > 0 ? `\n\n---\n⚠️ Failed to load ${failures.length} page(s): ${failures.join(', ')}` : '' + + if (results.length === 0 && failures.length === DOC_PAGES.length) { + return { content: [{ type: 'text', text: `All page fetches failed. Check network connectivity.${failureSummary}` }], isError: true } + } + if (results.length === 0) { - return { content: [{ type: 'text', text: `No results found for "${query}".` }] } + return { content: [{ type: 'text', text: `No results found for "${query}".${failureSummary}` }] } } return { content: [ { type: 'text', - text: `Found ${results.length} page(s) matching "${query}":\n\n${results.join('\n\n---\n\n')}`, + text: `Found ${results.length} page(s) matching "${query}":\n\n${results.join('\n\n---\n\n')}${failureSummary}`, }, ], }