@intentwake/mcp
Advanced tools
+242
-27
@@ -37,5 +37,11 @@ #!/usr/bin/env node | ||
| mcpUrl: safeServiceUrl(env.INTENTWAKE_MCP_URL ?? "https://mcp.intentwake.com/mcp"), | ||
| apiUrl: safeServiceUrl(env.INTENTWAKE_API_URL ?? "https://api.intentwake.com") | ||
| apiUrl: safeServiceUrl(env.INTENTWAKE_API_URL ?? "https://api.intentwake.com"), | ||
| hostedToolPolicy: hostedToolPolicy(env.INTENTWAKE_HOSTED_TOOL_POLICY) | ||
| }; | ||
| } | ||
| function hostedToolPolicy(value) { | ||
| if (value === void 0 || value === "" || value === "open") return "open"; | ||
| if (value === "allowlist") return "allowlist"; | ||
| throw new BridgeError("configuration_error", "Hosted tool policy configuration is invalid."); | ||
| } | ||
| function safeServiceUrl(value) { | ||
@@ -58,4 +64,10 @@ let url; | ||
| import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; | ||
| import { | ||
| McpError | ||
| } from "@modelcontextprotocol/sdk/types.js"; | ||
| function createControlPlaneClient(config, fetchImpl = fetch) { | ||
| const client = new Client({ name: "intentwake-local-mcp", version: "0.1.1" }); | ||
| const client = new Client( | ||
| { name: "intentwake-local-mcp", version: "0.2.0" }, | ||
| { enforceStrictCapabilities: true } | ||
| ); | ||
| const transport = new StreamableHTTPClientTransport(config.mcpUrl, { | ||
@@ -66,7 +78,14 @@ fetch: fetchImpl, | ||
| let connected = null; | ||
| const ensureConnected = async () => { | ||
| connected ??= client.connect(transport); | ||
| await connected; | ||
| const capabilities = client.getServerCapabilities(); | ||
| const server = client.getServerVersion(); | ||
| if (capabilities?.tools === void 0 || server?.name !== "intentwake-agent-gateway" || typeof server.version !== "string" || server.version.length === 0) { | ||
| throw incompatibleManifest(); | ||
| } | ||
| }; | ||
| const call = async (name, args) => { | ||
| try { | ||
| connected ??= client.connect(transport); | ||
| await connected; | ||
| const result = await client.callTool({ name, arguments: args }); | ||
| const result = await callHostedTool({ name, arguments: args }); | ||
| if (result.isError) { | ||
@@ -82,3 +101,37 @@ const value = asRecord(result.structuredContent); | ||
| }; | ||
| const callHostedTool = async (params, signal) => { | ||
| try { | ||
| await ensureConnected(); | ||
| return await client.callTool(params, void 0, signal ? { signal } : void 0); | ||
| } catch (error) { | ||
| if (error instanceof BridgeError || error instanceof McpError) throw error; | ||
| throw new BridgeError("control_plane_unavailable", "The IntentWake control plane is unavailable.", true); | ||
| } | ||
| }; | ||
| return { | ||
| async listHostedTools() { | ||
| try { | ||
| await ensureConnected(); | ||
| const tools = []; | ||
| const cursors = /* @__PURE__ */ new Set(); | ||
| let cursor; | ||
| for (let page = 0; page < 100; page += 1) { | ||
| const result = await client.listTools(cursor ? { cursor } : void 0); | ||
| tools.push(...result.tools); | ||
| if (!result.nextCursor) return tools; | ||
| if (cursors.has(result.nextCursor)) throw incompatibleManifest(); | ||
| cursors.add(result.nextCursor); | ||
| cursor = result.nextCursor; | ||
| } | ||
| throw incompatibleManifest(); | ||
| } catch (error) { | ||
| if (error instanceof BridgeError) throw error; | ||
| throw new BridgeError( | ||
| "hosted_manifest_unavailable", | ||
| "The IntentWake hosted tool surface is unavailable.", | ||
| true | ||
| ); | ||
| } | ||
| }, | ||
| callHostedTool, | ||
| async startListUpload(input) { | ||
@@ -167,3 +220,3 @@ return parseSession("list", await call("start_bulk_list_upload", { ...input })); | ||
| async close() { | ||
| if (connected) await transport.close(); | ||
| if (connected) await client.close(); | ||
| } | ||
@@ -245,2 +298,8 @@ }; | ||
| } | ||
| function incompatibleManifest() { | ||
| return new BridgeError( | ||
| "hosted_manifest_incompatible", | ||
| "The IntentWake hosted tool surface is incompatible." | ||
| ); | ||
| } | ||
@@ -327,5 +386,62 @@ // src/paths.ts | ||
| // src/tools.ts | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| import { Server } from "@modelcontextprotocol/sdk/server/index.js"; | ||
| import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js"; | ||
| import { | ||
| CallToolRequestSchema, | ||
| ErrorCode, | ||
| ListToolsRequestSchema, | ||
| McpError as McpError2 | ||
| } from "@modelcontextprotocol/sdk/types.js"; | ||
| import { z } from "zod"; | ||
| // src/manifest.ts | ||
| var PINNED_HOSTED_TOOL_NAMES = [ | ||
| "get_bulk_lookup_rest_workflow", | ||
| "search_intent_taxonomy", | ||
| "lookup_signals", | ||
| "list_lists", | ||
| "get_list", | ||
| "delete_list", | ||
| "create_list", | ||
| "start_bulk_list_upload", | ||
| "complete_bulk_list_upload", | ||
| "append_list_entries", | ||
| "start_bulk_append_upload", | ||
| "complete_bulk_append_upload", | ||
| "get_list_append", | ||
| "set_list_monitoring", | ||
| "list_downloads", | ||
| "get_download_url", | ||
| "download_result_file", | ||
| "unlock_download", | ||
| "unlock_backfill_results", | ||
| "get_recent_results", | ||
| "get_wallet", | ||
| "get_funding_request", | ||
| "request_funding" | ||
| ]; | ||
| function validateHostedManifest(tools, policy, localNames) { | ||
| if (tools.length === 0) throw incompatibleManifest2(); | ||
| const names = /* @__PURE__ */ new Set(); | ||
| for (const tool of tools) { | ||
| const annotations = tool.annotations; | ||
| if (names.has(tool.name) || localNames.has(tool.name) || typeof annotations?.title !== "string" || annotations.title.trim().length === 0 || typeof annotations.readOnlyHint !== "boolean" || typeof annotations.destructiveHint !== "boolean") { | ||
| throw incompatibleManifest2(); | ||
| } | ||
| names.add(tool.name); | ||
| } | ||
| if (policy === "open") return tools; | ||
| if (PINNED_HOSTED_TOOL_NAMES.some((name) => !names.has(name))) { | ||
| throw incompatibleManifest2(); | ||
| } | ||
| const pinned = new Set(PINNED_HOSTED_TOOL_NAMES); | ||
| return tools.filter(({ name }) => pinned.has(name)); | ||
| } | ||
| function incompatibleManifest2() { | ||
| return new BridgeError( | ||
| "hosted_manifest_incompatible", | ||
| "The IntentWake hosted tool surface is incompatible." | ||
| ); | ||
| } | ||
| // src/upload.ts | ||
@@ -883,3 +999,9 @@ async function uploadListFile(input, deps) { | ||
| }, | ||
| annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true }, | ||
| annotations: { | ||
| title: "Upload List File", | ||
| readOnlyHint: false, | ||
| idempotentHint: true, | ||
| destructiveHint: false, | ||
| openWorldHint: true | ||
| }, | ||
| run: (args, deps) => uploadListFile(args, deps) | ||
@@ -896,3 +1018,9 @@ }, | ||
| }, | ||
| annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false, openWorldHint: true }, | ||
| annotations: { | ||
| title: "Append List File", | ||
| readOnlyHint: false, | ||
| idempotentHint: true, | ||
| destructiveHint: false, | ||
| openWorldHint: true | ||
| }, | ||
| run: (args, deps) => appendListFile(args, deps) | ||
@@ -911,27 +1039,99 @@ }, | ||
| }, | ||
| annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true, openWorldHint: true }, | ||
| annotations: { | ||
| title: "Download Result To File", | ||
| readOnlyHint: false, | ||
| idempotentHint: true, | ||
| destructiveHint: true, | ||
| openWorldHint: true | ||
| }, | ||
| run: (args, deps) => downloadResultToFile(args, deps) | ||
| } | ||
| ]; | ||
| function createIntentWakeMcpServer(deps) { | ||
| const server = new McpServer({ name: "intentwake-local-mcp", version: "0.1.1" }); | ||
| for (const tool of TOOL_DEFINITIONS) { | ||
| server.registerTool(tool.name, { | ||
| description: tool.description, | ||
| inputSchema: tool.inputSchema, | ||
| annotations: tool.annotations | ||
| }, async (args) => { | ||
| async function createIntentWakeMcpServer(deps) { | ||
| const localTools = new Map(TOOL_DEFINITIONS.map((tool) => [tool.name, tool])); | ||
| const hostedTools = validateHostedManifest( | ||
| await deps.client.listHostedTools(), | ||
| deps.hostedToolPolicy, | ||
| new Set(localTools.keys()) | ||
| ); | ||
| const hostedNames = new Set(hostedTools.map(({ name }) => name)); | ||
| const server = new Server( | ||
| { name: "intentwake-local-mcp", version: "0.2.0" }, | ||
| { capabilities: { tools: {} } } | ||
| ); | ||
| server.setRequestHandler(ListToolsRequestSchema, async () => ({ | ||
| tools: [...hostedTools, ...TOOL_DEFINITIONS.map(localToolDescriptor)] | ||
| })); | ||
| server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { | ||
| if (hostedNames.has(request.params.name)) { | ||
| try { | ||
| const value = await tool.run(args, deps); | ||
| const structuredContent = value && typeof value === "object" ? value : { result: value }; | ||
| return { content: [{ type: "text", text: JSON.stringify(structuredContent) }], structuredContent }; | ||
| return await deps.client.callHostedTool(request.params, extra.signal); | ||
| } catch (error) { | ||
| const safe = safeError(error); | ||
| const value = { error: safe.code, message: safe.message, retryable: safe.retryable }; | ||
| return { isError: true, content: [{ type: "text", text: JSON.stringify(value) }], structuredContent: value }; | ||
| if (error instanceof McpError2) throw relayHostedMcpError(error); | ||
| return toolError(safeHostedError(error)); | ||
| } | ||
| } | ||
| const tool = localTools.get(request.params.name); | ||
| if (!tool) throw new McpError2(ErrorCode.InvalidParams, `Tool ${request.params.name} not found`); | ||
| if (request.params.task) { | ||
| throw new McpError2(ErrorCode.InvalidParams, `Tool ${request.params.name} does not support tasks`); | ||
| } | ||
| const parsed = await z.strictObject(tool.inputSchema).safeParseAsync(request.params.arguments ?? {}); | ||
| if (!parsed.success) throw new McpError2(ErrorCode.InvalidParams, "Invalid tool arguments."); | ||
| try { | ||
| const value = await tool.run(parsed.data, deps); | ||
| const structuredContent = value && typeof value === "object" ? value : { result: value }; | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify(structuredContent) }], | ||
| structuredContent | ||
| }; | ||
| } catch (error) { | ||
| return toolError(safeError(error)); | ||
| } | ||
| }); | ||
| server.onclose = () => { | ||
| void Promise.resolve().then(() => deps.client.close()).catch(() => { | ||
| }); | ||
| } | ||
| }; | ||
| return server; | ||
| } | ||
| function localToolDescriptor(tool) { | ||
| return { | ||
| name: tool.name, | ||
| description: tool.description, | ||
| inputSchema: toJsonSchemaCompat(z.strictObject(tool.inputSchema), { | ||
| strictUnions: true, | ||
| pipeStrategy: "input" | ||
| }), | ||
| annotations: tool.annotations, | ||
| execution: { taskSupport: "forbidden" } | ||
| }; | ||
| } | ||
| function safeHostedError(error) { | ||
| if (error instanceof BridgeError) return error; | ||
| return new BridgeError( | ||
| "control_plane_unavailable", | ||
| "The IntentWake control plane is unavailable.", | ||
| true | ||
| ); | ||
| } | ||
| function relayHostedMcpError(error) { | ||
| const prefix = `MCP error ${error.code}: `; | ||
| return Object.assign( | ||
| new Error(error.message.startsWith(prefix) ? error.message.slice(prefix.length) : error.message), | ||
| { code: error.code, data: error.data } | ||
| ); | ||
| } | ||
| function toolError(error) { | ||
| const value = { | ||
| error: error.code, | ||
| message: error.message, | ||
| retryable: error.retryable | ||
| }; | ||
| return { | ||
| isError: true, | ||
| content: [{ type: "text", text: JSON.stringify(value) }], | ||
| structuredContent: value | ||
| }; | ||
| } | ||
@@ -943,4 +1143,19 @@ // src/index.ts | ||
| const client = createControlPlaneClient(config); | ||
| const server = createIntentWakeMcpServer({ roots, client }); | ||
| await server.connect(new StdioServerTransport()); | ||
| try { | ||
| const server = await createIntentWakeMcpServer({ | ||
| roots, | ||
| client, | ||
| hostedToolPolicy: config.hostedToolPolicy | ||
| }); | ||
| process.stdin.once("end", () => { | ||
| void server.close().catch(() => { | ||
| }); | ||
| }); | ||
| await server.connect(new StdioServerTransport()); | ||
| if (process.stdin.readableEnded) await server.close(); | ||
| } catch (error) { | ||
| await client.close().catch(() => { | ||
| }); | ||
| throw error; | ||
| } | ||
| } | ||
@@ -947,0 +1162,0 @@ main().catch(() => { |
+3
-2
| { | ||
| "name": "@intentwake/mcp", | ||
| "version": "0.1.1", | ||
| "description": "Local stdio MCP bridge for IntentWake file transfers", | ||
| "version": "0.2.0", | ||
| "description": "Unified IntentWake stdio MCP proxy with local file transfers", | ||
| "keywords": [ | ||
@@ -9,2 +9,3 @@ "intentwake", | ||
| "stdio", | ||
| "proxy", | ||
| "file-transfer" | ||
@@ -11,0 +12,0 @@ ], |
+17
-2
| # `@intentwake/mcp` | ||
| Local stdio MCP bridge for complete IntentWake file transfers. | ||
| Unified local stdio MCP server for IntentWake account workflows and complete | ||
| file transfers. | ||
| The bridge exposes three semantic tools: | ||
| The server proxies the authenticated hosted IntentWake MCP tool surface without | ||
| reimplementing its account, list, wallet, funding, policy, billing, | ||
| idempotency, authorization, or audit behavior. It adds three local-only | ||
| semantic tools: | ||
@@ -37,2 +41,5 @@ - `upload_list_file` streams an allowed local file into a canonical IntentWake list. | ||
| - `INTENTWAKE_API_URL`: optional public API URL override used for canonical CSV streams. | ||
| - `INTENTWAKE_HOSTED_TOOL_POLICY`: optional hosted manifest policy. The default, | ||
| `open`, exposes every compatible hosted tool. `allowlist` pins the hosted | ||
| surface to the names shipped with this package version. | ||
@@ -42,2 +49,10 @@ With no allowed roots configured, every file operation is refused. There are no implicit current | ||
| At startup the server negotiates one authenticated hosted MCP connection, | ||
| validates the complete hosted manifest, and fails closed if the gateway is | ||
| unavailable, incompatible, missing required tool annotations, or collides with | ||
| a local tool. That same hosted connection handles proxied calls and the | ||
| control-plane steps used by local file transfers. Hosted names, schemas, | ||
| annotations, structured results, safe errors, and metadata pass through | ||
| unchanged. | ||
| ## File safety | ||
@@ -44,0 +59,0 @@ |
48157
18.72%1129
23.39%76
24.59%