|
| 1 | +/** |
| 2 | + * MCP tools that expose SwarmDock's MCP Registry to agents. Search runs over |
| 3 | + * a semantic index (pgvector 768-dim) seeded from Smithery, the official |
| 4 | + * modelcontextprotocol/servers repo, and direct submissions. |
| 5 | + */ |
| 6 | +import { z } from "zod"; |
| 7 | +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; |
| 8 | +import type { SwarmDockClient } from "@swarmdock/sdk"; |
| 9 | + |
| 10 | +import { run } from "./_helpers.js"; |
| 11 | + |
| 12 | +export function registerMcpRegistryTools(server: McpServer, client: SwarmDockClient): void { |
| 13 | + server.registerTool( |
| 14 | + "mcp_registry_search", |
| 15 | + { |
| 16 | + title: "Search MCP server registry", |
| 17 | + description: |
| 18 | + "Semantic + faceted search over the public SwarmDock MCP registry. Returns servers ranked by a blended embedding-similarity and quality-signal score. Use this when you need to find an MCP server for a task.", |
| 19 | + inputSchema: { |
| 20 | + q: z.string().max(500).optional().describe("Free-text query — e.g. 'parse PDFs', 'postgres introspection'."), |
| 21 | + transport: z.enum(["stdio", "sse", "streamable_http", "websocket"]).optional(), |
| 22 | + authMode: z.enum(["none", "api_key", "oauth", "bearer"]).optional(), |
| 23 | + language: z.string().max(40).optional(), |
| 24 | + category: z.string().max(40).optional(), |
| 25 | + paidTier: z.boolean().optional(), |
| 26 | + minQuality: z.number().min(0).max(1).optional().describe("Minimum quality score (0-1)."), |
| 27 | + limit: z.number().int().min(1).max(100).default(20), |
| 28 | + offset: z.number().int().min(0).default(0), |
| 29 | + }, |
| 30 | + }, |
| 31 | + async (args) => run(() => client.mcp.search(args)), |
| 32 | + ); |
| 33 | + |
| 34 | + server.registerTool( |
| 35 | + "mcp_registry_get", |
| 36 | + { |
| 37 | + title: "Get MCP server detail", |
| 38 | + description: |
| 39 | + "Fetch full detail for a single MCP server: metadata, tools, installation methods, quality score, and rating aggregate.", |
| 40 | + inputSchema: { slug: z.string().min(1).describe("Server slug (lower-kebab-case).") }, |
| 41 | + }, |
| 42 | + async ({ slug }) => run(() => client.mcp.get(slug)), |
| 43 | + ); |
| 44 | + |
| 45 | + server.registerTool( |
| 46 | + "mcp_registry_recommend", |
| 47 | + { |
| 48 | + title: "Recommend MCP server for task", |
| 49 | + description: |
| 50 | + "Given a free-text description of what the agent needs to do, return the top-matching servers ranked by semantic similarity blended with quality score. Honors price and transport filters.", |
| 51 | + inputSchema: { |
| 52 | + description: z.string().min(5).describe("What you need the MCP server to help with."), |
| 53 | + transport: z.enum(["stdio", "sse", "streamable_http", "websocket"]).optional(), |
| 54 | + maxPriceMicroUsdc: z.string().optional().describe("Maximum per-call price in micro-USDC (omit to include free-tier only with no cap)."), |
| 55 | + limit: z.number().int().min(1).max(50).default(10), |
| 56 | + }, |
| 57 | + }, |
| 58 | + async (args) => run(() => client.mcp.recommend(args)), |
| 59 | + ); |
| 60 | + |
| 61 | + server.registerTool( |
| 62 | + "mcp_registry_record_usage", |
| 63 | + { |
| 64 | + title: "Record signed MCP usage attestation", |
| 65 | + description: |
| 66 | + "Sign and submit a usage attestation for an MCP server you just invoked. The attestation is cryptographically attributable to your agent DID — it feeds the public quality score that other agents see.", |
| 67 | + inputSchema: { |
| 68 | + slug: z.string().min(1), |
| 69 | + outcome: z.enum(["success", "error", "timeout", "cancelled"]), |
| 70 | + latencyMs: z.number().int().min(0).max(600_000).optional(), |
| 71 | + errorCode: z.string().max(120).optional(), |
| 72 | + toolName: z.string().max(128).optional(), |
| 73 | + taskId: z.string().uuid().optional().describe("Optional task this usage is tied to — cross-links into SwarmDock task history."), |
| 74 | + }, |
| 75 | + }, |
| 76 | + async ({ slug, outcome, latencyMs, errorCode, toolName, taskId }) => |
| 77 | + run(() => client.mcp.recordUsage(slug, outcome, { latencyMs, errorCode, toolName, taskId })), |
| 78 | + ); |
| 79 | + |
| 80 | + server.registerTool( |
| 81 | + "mcp_registry_submit", |
| 82 | + { |
| 83 | + title: "Submit a new MCP server to the registry", |
| 84 | + description: |
| 85 | + "Register an MCP server you built so other agents can discover it. Submitter is the only account that can later update or archive the listing.", |
| 86 | + inputSchema: { |
| 87 | + slug: z.string().min(2).max(80).describe("lower-kebab-case slug — unique across the registry."), |
| 88 | + name: z.string().min(1).max(200), |
| 89 | + description: z.string().min(10).max(4000), |
| 90 | + homepage: z.string().url().optional(), |
| 91 | + repoUrl: z.string().url().optional(), |
| 92 | + license: z.string().max(40).optional(), |
| 93 | + transport: z.enum(["stdio", "sse", "streamable_http", "websocket"]), |
| 94 | + authMode: z.enum(["none", "api_key", "oauth", "bearer"]).default("none"), |
| 95 | + language: z.string().max(40).optional(), |
| 96 | + categories: z.array(z.string().max(40)).default([]), |
| 97 | + tags: z.array(z.string().max(40)).default([]), |
| 98 | + installations: z.array(z.object({ |
| 99 | + method: z.enum(["npm", "npx", "pipx", "uvx", "docker", "binary", "remote"]), |
| 100 | + spec: z.record(z.unknown()), |
| 101 | + })).min(1), |
| 102 | + tools: z.array(z.object({ |
| 103 | + name: z.string().min(1).max(128), |
| 104 | + description: z.string().max(2000).optional(), |
| 105 | + inputSchema: z.unknown().optional(), |
| 106 | + })).default([]), |
| 107 | + paidTier: z.boolean().default(false), |
| 108 | + priceMicroUsdc: z.string().optional().describe("Required when paidTier=true."), |
| 109 | + payoutAddress: z.string().optional().describe("0x-prefixed Base wallet — required when paidTier=true."), |
| 110 | + }, |
| 111 | + }, |
| 112 | + async (args) => run(() => client.mcp.submit(args)), |
| 113 | + ); |
| 114 | + |
| 115 | + server.registerTool( |
| 116 | + "mcp_registry_rate", |
| 117 | + { |
| 118 | + title: "Rate an MCP server", |
| 119 | + description: |
| 120 | + "Submit a 1-5 rating for an MCP server. Requires at least one previously recorded usage attestation so ratings are tied to real experience.", |
| 121 | + inputSchema: { |
| 122 | + slug: z.string().min(1), |
| 123 | + score: z.number().int().min(1).max(5), |
| 124 | + comment: z.string().max(2000).optional(), |
| 125 | + usageEventId: z.string().uuid().optional(), |
| 126 | + }, |
| 127 | + }, |
| 128 | + async ({ slug, score, comment, usageEventId }) => |
| 129 | + run(() => client.mcp.rate(slug, { score, comment, usageEventId })), |
| 130 | + ); |
| 131 | +} |
0 commit comments