diff --git a/apps/mcp-client/package.json b/apps/mcp-client/package.json new file mode 100644 index 0000000..c117543 --- /dev/null +++ b/apps/mcp-client/package.json @@ -0,0 +1,26 @@ +{ + "name": "@mcpedia/mcp-client", + "version": "0.1.0", + "private": true, + "type": "module", + "bin": { + "mcpedia-mcp-client": "./src/index.ts" + }, + "scripts": { + "start": "bun run src/index.ts", + "chat": "bun run src/index.ts", + "ask": "bun run src/ask.ts", + "lint": "bun run lint", + "typecheck": "tsc --noEmit", + "build": "bun build src/index.ts --outdir=dist --format=esm", + "test": "bun test" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^4.0.0" + }, + "devDependencies": { + "@types/node": "^20", + "typescript": "^5.6.0" + } +} diff --git a/apps/mcp-client/src/ask.ts b/apps/mcp-client/src/ask.ts new file mode 100644 index 0000000..c65e7aa --- /dev/null +++ b/apps/mcp-client/src/ask.ts @@ -0,0 +1,131 @@ +import { McpediaClient } from "./client"; + +// One-shot MCP client — runs a single tool call and prints the result. +// +// Usage: +// bun run ask search "websocket" +// bun run ask doc docs/websocket/contract +// bun run ask tools +// bun run ask resources +// bun run ask hybrid "what is MCP" +// bun run ask create --slug=docs/my-doc --title="My Doc" --body="Hello" +// (note: create requires WEBHOOK_SECRET env) +// bun run ask delete docs/my-doc + +const MCP_URL = process.env.MCP_URL ?? "https://mcp.asepharyana.my.id/mcp"; + +async function main() { + const [, , command, ...args] = process.argv; + if (!command) { + console.error("Usage: ask [args...]"); + console.error("Commands: tools, resources, search , ss , hybrid , doc , related , create, delete , index , status"); + process.exit(1); + } + + const headers: Record = {}; + const secret = process.env.WEBHOOK_SECRET; + if (secret) headers["x-webhook-secret"] = secret; + + const client = await McpediaClient.connect(MCP_URL, { headers }); + + try { + switch (command) { + case "tools": { + const tools = await client.listTools(); + console.log(JSON.stringify(tools.map((t: any) => ({ name: t.name, description: t.description })), null, 2)); + break; + } + case "resources": { + const res = await client.listResources(); + console.log(JSON.stringify(res, null, 2)); + break; + } + case "search": { + const q = args.join(" "); + if (!q) throw new Error("search requires a query"); + const hits = await client.search(q); + console.log(JSON.stringify(hits, null, 2)); + break; + } + case "ss": { + const q = args.join(" "); + if (!q) throw new Error("ss requires a query"); + const hits = await client.semanticSearch(q); + console.log(JSON.stringify(hits, null, 2)); + break; + } + case "hybrid": { + const q = args.join(" "); + if (!q) throw new Error("hybrid requires a query"); + const hits = await client.hybridSearch(q); + console.log(JSON.stringify(hits, null, 2)); + break; + } + case "doc": { + const slug = args[0]; + if (!slug) throw new Error("doc requires a slug"); + const body = await client.getDocument(slug); + console.log(body); + break; + } + case "related": { + const slug = args[0]; + if (!slug) throw new Error("related requires a slug"); + const rel = await client.getRelated(slug); + console.log(JSON.stringify(rel, null, 2)); + break; + } + case "create": { + const opts: Record = {}; + for (const arg of args) { + const [k, v] = arg.split("=") as [string, string]; + if (k && v) opts[k.replace("--", "")] = v; + } + const res = await client.createDocument({ + slug: opts.slug || "", + title: opts.title || "Untitled", + section: (opts.section || "docs") as any, + body: opts.body || "", + tags: opts.tags ? opts.tags.split(",") : [], + author: opts.author, + }); + console.log(JSON.stringify(res, null, 2)); + break; + } + case "delete": { + const slug = args[0]; + if (!slug) throw new Error("delete requires a slug"); + const res = await client.deleteDocument(slug); + console.log(JSON.stringify(res, null, 2)); + break; + } + case "index": { + const slug = args[0]; + if (!slug) throw new Error("index requires a slug"); + const res = await client.indexDocument(slug); + console.log(JSON.stringify(res, null, 2)); + break; + } + case "status": { + const res = await client.queueStatus(); + console.log(JSON.stringify(res, null, 2)); + break; + } + default: + console.error(`Unknown command: ${command}`); + process.exit(1); + } + } finally { + await client.disconnect(); + } +} + +main().catch((err) => { + const msg = err instanceof Error ? err.message : String(err); + if (msg.includes("unauthorized")) { + console.error("⚠️ Write tools require x-webhook-secret. Set WEBHOOK_SECRET env var."); + } else { + console.error(`Error: ${msg}`); + } + process.exit(1); +}); diff --git a/apps/mcp-client/src/client.test.ts b/apps/mcp-client/src/client.test.ts new file mode 100644 index 0000000..64bd132 --- /dev/null +++ b/apps/mcp-client/src/client.test.ts @@ -0,0 +1,116 @@ +import { describe, it, expect, mock } from "bun:test"; + +// Tests for the MCP client wrapper. Uses a fake Client that implements the +// subset of methods McpediaClient uses, so no network/DB is needed. + +// Mock the SDK client to avoid real network connections. +const mockTools = [ + { name: "search_documents", description: "keyword search" }, + { name: "get_document", description: "get doc body" }, + { name: "create_document", description: "create doc" }, + { name: "delete_document", description: "delete doc" }, +]; + +mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ + Client: class { + connect() {} + close() {} + listTools() { + return Promise.resolve({ tools: mockTools }); + } + callTool(opts: any) { + const { name, arguments: args } = opts; + if (name === "get_document") { + return Promise.resolve({ content: [{ type: "text", text: `# ${args.slug}` }] }); + } + if (name === "create_document") { + return Promise.resolve({ content: [{ type: "text", text: JSON.stringify({ ok: true, slug: args.slug }) }] }); + } + if (name === "delete_document") { + return Promise.resolve({ content: [{ type: "text", text: JSON.stringify({ ok: true, deleted: true }) }] }); + } + return Promise.resolve({ content: [{ type: "text", text: "[]" }] }); + } + listResources() { + return Promise.resolve({ resources: [{ uri: "mcpedia://docs", mimeType: "application/json" }] }); + } + readResource({ uri }: any) { + return Promise.resolve({ contents: [{ uri, mimeType: "text/markdown", text: "# Test" }] }); + } + }, +})); + +mock.module("@modelcontextprotocol/sdk/client/streamableHttp.js", () => ({ + StreamableHTTPClientTransport: class { + constructor(url: URL, opts?: any) {} + }, +})); + +const { McpediaClient } = await import("../src/client"); + +describe("McpediaClient", () => { + it("listTools returns all registered tools", async () => { + const client = await McpediaClient.connect("http://localhost:0/mcp"); + const tools = await client.listTools(); + expect(tools.length).toBe(4); + expect(tools.map((t: any) => t.name)).toContain("search_documents"); + expect(tools.map((t: any) => t.name)).toContain("get_document"); + await client.disconnect(); + }); + + it("getDocument returns markdown body", async () => { + const client = await McpediaClient.connect("http://localhost:0/mcp"); + const body = await client.getDocument("docs/websocket/contract"); + expect(body).toBe("# docs/websocket/contract"); + await client.disconnect(); + }); + + it("createDocument posts args and parses JSON response", async () => { + const client = await McpediaClient.connect("http://localhost:0/mcp"); + const res = await client.createDocument({ + slug: "docs/test", + title: "Test", + section: "docs", + body: "# Hello", + }); + expect(res.ok).toBe(true); + expect(res.slug).toBe("docs/test"); + await client.disconnect(); + }); + + it("deleteDocument returns deleted flag", async () => { + const client = await McpediaClient.connect("http://localhost:0/mcp"); + const res = await client.deleteDocument("docs/test"); + expect(res.ok).toBe(true); + expect(res.deleted).toBe(true); + await client.disconnect(); + }); + + it("listResources returns resource list", async () => { + const client = await McpediaClient.connect("http://localhost:0/mcp"); + const resources = await client.listResources(); + expect(resources.length).toBe(1); + expect(resources[0].uri).toBe("mcpedia://docs"); + await client.disconnect(); + }); + + it("readResource returns text content", async () => { + const client = await McpediaClient.connect("http://localhost:0/mcp"); + const res = await client.readResource("mcpedia://docs/docs/websocket/contract"); + expect(res.text).toBe("# Test"); + expect(res.mimeType).toBe("text/markdown"); + await client.disconnect(); + }); + + it("send headers via requestInit for auth", async () => { + // Verify that options.headers are passed through to the transport constructor + const client = await McpediaClient.connect("http://localhost:0/mcp", { + headers: { "x-webhook-secret": "secret123" }, + }); + // We can't directly assert the transport constructor received headers, + // but we verify the client connects and works with headers set. + const tools = await client.listTools(); + expect(tools.length).toBeGreaterThan(0); + await client.disconnect(); + }); +}); diff --git a/apps/mcp-client/src/client.ts b/apps/mcp-client/src/client.ts new file mode 100644 index 0000000..6cc82cf --- /dev/null +++ b/apps/mcp-client/src/client.ts @@ -0,0 +1,168 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import type { CallToolResult, ListResourcesResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; + +// MCP client that talks to the MCPedia MCP server over Streamable HTTP. +// +// Usage: +// import { McpediaClient } from "@/src/client"; +// const client = await McpediaClient.connect( +// "https://mcp.asepharyana.my.id/mcp", +// { headers: { "x-webhook-secret": process.env.WEBHOOK_SECRET } } +// ); +// +// const docs = await client.listDocuments(); +// const results = await client.search("websocket"); +// const doc = await client.getDocument("docs/websocket/contract"); + +export interface McpediaClientOptions { + /** Custom headers to send with each request (e.g. x-webhook-secret for write tools). */ + headers?: Record; +} + +// Type-cast helper for the SDK's content union (avoids TS noise from zod-v4 typing). +type AnyContent = { text?: string; uri?: string; mimeType?: string; blob?: string }; +function extractText(result: CallToolResult): string { + return ((result.content?.[0] as AnyContent) ?? {}).text ?? ""; +} + +export class McpediaClient { + private client: Client; + private options: McpediaClientOptions; + + private constructor(client: Client, options: McpediaClientOptions) { + this.client = client; + this.options = options; + } + + /** Connect to an MCP server over Streamable HTTP. */ + static async connect( + url: string, + options: McpediaClientOptions = {}, + ): Promise { + const transport = new StreamableHTTPClientTransport(new URL(url), { + requestInit: options.headers ? { headers: options.headers } : undefined, + }); + const client = new Client( + { name: "mcpedia-client", version: "0.1.0" }, + { capabilities: {} }, + ); + await client.connect(transport); + return new McpediaClient(client, options); + } + + // --- Tool wrappers --- + + async listTools(): Promise { + const res = await this.client.listTools(); + return res.tools; + } + + async callTool(name: string, args: Record = {}): Promise { + return await this.client.callTool({ name, arguments: args }) as CallToolResult; + } + + // Convenience methods for MCPedia-specific tools: + + /** List all documents, optionally filtered by section. */ + async listDocuments(section?: string): Promise { + const args: Record = {}; + if (section) args.section = section; + const result = await this.callTool("list_documents", args); + return JSON.parse(extractText(result) || "[]"); + } + + /** Get full markdown body of a document by slug. */ + async getDocument(slug: string): Promise { + const result = await this.callTool("get_document", { slug }); + return extractText(result); + } + + /** Keyword (full-text) search. */ + async search(query: string, limit = 20): Promise { + const result = await this.callTool("search_documents", { query, limit }); + return JSON.parse(extractText(result) || "[]"); + } + + /** Semantic search (embedding-based). */ + async semanticSearch(query: string, limit = 10): Promise { + const result = await this.callTool("semantic_search", { query, limit }); + return JSON.parse(extractText(result) || "[]"); + } + + /** Hybrid search (FTS + semantic via RRF). */ + async hybridSearch(query: string, limit = 10): Promise { + const result = await this.callTool("hybrid_search", { query, limit }); + return JSON.parse(extractText(result) || "[]"); + } + + /** Get related documents by shared tags. */ + async getRelated(slug: string, limit = 5): Promise { + const result = await this.callTool("get_related_documents", { slug, limit }); + return JSON.parse(extractText(result) || "[]"); + } + + /** Index (re-index) a single document. */ + async indexDocument(slug: string): Promise { + const result = await this.callTool("index_document", { slug }); + return JSON.parse(extractText(result) || "{}"); + } + + /** Full-corpus reindex. */ + async reindexAll(): Promise { + const result = await this.callTool("reindex_all", {}); + return JSON.parse(extractText(result) || "{}"); + } + + /** Current queue status. */ + async queueStatus(): Promise { + const result = await this.callTool("queue_status", {}); + return JSON.parse(extractText(result) || "{}"); + } + + /** Create a document (requires x-webhook-secret). */ + async createDocument(input: { + slug: string; + title: string; + section: "docs" | "writeups" | "research" | "notes"; + body: string; + type?: "documentation" | "writeup" | "research" | "note"; + status?: "published" | "draft"; + author?: string; + tags?: string[]; + }): Promise { + const result = await this.callTool("create_document", input); + return JSON.parse(extractText(result) || "{}"); + } + + /** Update a document (requires x-webhook-secret). */ + async updateDocument(slug: string, updates: Record): Promise { + const result = await this.callTool("update_document", { slug, ...updates }); + return JSON.parse(extractText(result) || "{}"); + } + + /** Delete a document (requires x-webhook-secret). */ + async deleteDocument(slug: string): Promise { + const result = await this.callTool("delete_document", { slug }); + return JSON.parse(extractText(result) || "{}"); + } + + // --- Resource wrappers --- + + async listResources(): Promise { + const res = await this.client.listResources() as ListResourcesResult; + return res.resources; + } + + /** Read a resource by URI (e.g. mcpedia://docs). */ + async readResource(uri: string): Promise<{ uri: string; mimeType: string; text: string }> { + const res = await this.client.readResource({ uri }) as ReadResourceResult; + const item = res.contents[0] as AnyContent; + return { uri: item.uri ?? uri, mimeType: item.mimeType ?? "text/plain", text: item.text ?? "" }; + } + + /** Graceful disconnect. */ + async disconnect() { + await this.client.close(); + } +} diff --git a/apps/mcp-client/src/index.ts b/apps/mcp-client/src/index.ts new file mode 100644 index 0000000..2fdfd5a --- /dev/null +++ b/apps/mcp-client/src/index.ts @@ -0,0 +1,239 @@ +import { McpediaClient } from "./client"; + +const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET ?? ""; + +// Interactive MCP client — connects to the MCPedia MCP server over +// Streamable HTTP and provides a chat-like REPL. +// +// Usage: +// MCP_URL=https://mcp.asepharyana.my.id/mcp bun run chat +// bun run chat (defaults to https://mcp.asepharyana.my.id/mcp) +// +// Commands in the REPL: +// /tools — list available tools +// /resources — list available resources +// /search — keyword search +// /ss — semantic search +// /hybrid — hybrid search +// /doc — get document body +// /related — related documents +// /create — create a document (prompts for fields) +// /delete — delete a document +// /index — enqueue reindex for a doc +// /status — queue status +// /help — show help +// /quit — exit + +const HELP = `Commands: + /tools — list available MCP tools + /resources — list available resources + /search — keyword (full-text) search + /ss — semantic (embedding) search + /hybrid — hybrid search (FTS + semantic) + /doc — get document body (e.g. docs/websocket/contract) + /related — get related documents + /create — create a document (interactive prompts) + /update — update a document (interactive prompts) + /delete — delete a document + /index — enqueue reindex for a document + /status — queue status + /help — show this help + /quit — exit`; + +async function main() { + const url = process.env.MCP_URL ?? "https://mcp.asepharyana.my.id/mcp"; + const headers: Record = {}; + if (WEBHOOK_SECRET) { + headers["x-webhook-secret"] = WEBHOOK_SECRET; + } + + console.log(`Connecting to MCP server: ${url}`); + const client = await McpediaClient.connect(url, { headers }); + console.log("Connected! Type /help for commands.\n"); + + // Show available tools on startup + const tools = await client.listTools(); + console.log(`Available tools (${tools.length}): ${tools.map((t: any) => t.name).join(", ")}`); + console.log(""); + + const rl = await import("node:readline"); + const readline = rl.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: "mcp> ", + }); + + readline.prompt(); + + for await (const line of readline) { + const cmd = line.trim(); + if (!cmd) { + readline.prompt(); + continue; + } + + const [name, ...args] = cmd.split(/\s+/); + const arg = args.join(" "); + + try { + switch (name) { + case "/tools": { + const t = await client.listTools(); + console.log(t.map((x: any) => ` ${x.name}: ${x.description || ""}`).join("\n")); + break; + } + case "/resources": { + const r = await client.listResources(); + console.log(r.map((x: any) => ` ${x.uri}`).join("\n")); + break; + } + case "/search": { + if (!arg) throw new Error("Usage: /search "); + const hits = await client.search(arg); + printHits(hits); + break; + } + case "/ss": { + if (!arg) throw new Error("Usage: /ss "); + const hits = await client.semanticSearch(arg); + printHits(hits); + break; + } + case "/hybrid": { + if (!arg) throw new Error("Usage: /hybrid "); + const hits = await client.hybridSearch(arg); + printHits(hits); + break; + } + case "/doc": { + if (!arg) throw new Error("Usage: /doc "); + const text = await client.getDocument(arg); + console.log(text); + break; + } + case "/related": { + if (!arg) throw new Error("Usage: /related "); + const rel = await client.getRelated(arg); + console.log(rel.map((x: any) => ` ${x.title} (${x.slug})`).join("\n")); + break; + } + case "/create": { + await interactiveCreate(client); + break; + } + case "/update": { + if (!arg) throw new Error("Usage: /update "); + await interactiveUpdate(client, arg); + break; + } + case "/delete": { + if (!arg) throw new Error("Usage: /delete "); + const res = await client.deleteDocument(arg); + console.log(JSON.stringify(res, null, 2)); + break; + } + case "/index": { + if (!arg) throw new Error("Usage: /index "); + const res = await client.indexDocument(arg); + console.log(JSON.stringify(res, null, 2)); + break; + } + case "/status": { + const res = await client.queueStatus(); + console.log(JSON.stringify(res, null, 2)); + break; + } + case "/help": + console.log(HELP); + break; + case "/quit": + await client.disconnect(); + process.exit(0); + default: + console.log(`Unknown command: ${name}. Type /help for commands.`); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + if (msg.includes("unauthorized")) { + console.log("⚠️ Write tools require x-webhook-secret. Set WEBHOOK_SECRET in your .env."); + } else { + console.log(`Error: ${msg}`); + } + } + + readline.prompt(); + } +} + +function printHits(hits: any[]) { + if (!hits.length) { + console.log("No results."); + return; + } + for (const h of hits) { + const slug = h.doc?.slug || h.slug || "?"; + const title = h.doc?.title || h.title || slug; + const score = h.rank !== undefined ? `★ ${h.rank.toFixed(3)}` : h.score ? `★ ${h.score.toFixed(3)}` : ""; + const section = h.doc?.section || h.section || ""; + console.log(` ${title} ${score}`); + console.log(` /${slug} · ${section}`); + if (h.snippet || h.doc?.excerpt) { + const snip = (h.snippet || h.doc?.excerpt).slice(0, 120); + console.log(` ${snip}...`); + } + } +} + +async function ask(question: string): Promise { + const rl = await import("node:readline"); + return new Promise((resolve) => { + const iface = rl.createInterface({ input: process.stdin, output: process.stdout }); + iface.question(question, (ans) => { + iface.close(); + resolve(ans.trim()); + }); + }); +} + +async function interactiveCreate(client: McpediaClient) { + console.log("\n--- Create Document ---"); + const section = (await ask("Section (docs/writeups/research/notes) [docs]: ")) || "docs"; + const title = await ask("Title: "); + if (!title) throw new Error("Title is required."); + const slug = (await ask(`Slug (will become ${section}/): `)) || title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); + const body = await ask("Body (single line, or /edit for editor): ") || ""; + const tagsStr = await ask("Tags (comma-separated): "); + const tags = tagsStr.split(",").map((t) => t.trim()).filter(Boolean); + const author = await ask("Author: "); + + const res = await client.createDocument({ + slug: `${section}/${slug}`, + title, + section: section as "docs" | "writeups" | "research" | "notes", + body, + tags, + author: author || undefined, + }); + console.log(JSON.stringify(res, null, 2)); +} + +async function interactiveUpdate(client: McpediaClient, slug: string) { + console.log(`\n--- Update: ${slug} ---`); + const doc = await client.getDocument(slug); + const body = await ask(`Body (current: ${doc.slice(0, 50)}...): `) || doc; + const title = await ask("New title (or leave blank to keep): ") || undefined; + const tagsStr = await ask("New tags (comma-separated, or blank to keep): ") || ""; + const tags = tagsStr ? tagsStr.split(",").map((t) => t.trim()).filter(Boolean) : undefined; + + const res = await client.updateDocument(slug, { + ...(title && { title }), + body, + ...(tags && { tags }), + }); + console.log(JSON.stringify(res, null, 2)); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/apps/mcp-client/tsconfig.json b/apps/mcp-client/tsconfig.json new file mode 100644 index 0000000..367dffd --- /dev/null +++ b/apps/mcp-client/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "types": ["bun", "node"], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/bun.lock b/bun.lock index 4dda7d2..d22b8f9 100644 --- a/bun.lock +++ b/bun.lock @@ -49,6 +49,21 @@ "typescript": "^5.6.0", }, }, + "apps/mcp-client": { + "name": "@mcpedia/mcp-client", + "version": "0.1.0", + "bin": { + "mcpedia-mcp-client": "./src/index.ts", + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "zod": "^4.0.0", + }, + "devDependencies": { + "@types/node": "^20", + "typescript": "^5.6.0", + }, + }, "apps/web": { "name": "@mcpedia/web", "version": "0.1.0", @@ -384,6 +399,8 @@ "@mcpedia/mcp": ["@mcpedia/mcp@workspace:apps/mcp"], + "@mcpedia/mcp-client": ["@mcpedia/mcp-client@workspace:apps/mcp-client"], + "@mcpedia/parser": ["@mcpedia/parser@workspace:packages/parser"], "@mcpedia/queue": ["@mcpedia/queue@workspace:packages/queue"], @@ -1508,6 +1525,10 @@ "@mcpedia/mcp/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@mcpedia/mcp-client/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="], + + "@mcpedia/mcp-client/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@mcpedia/web/@types/node": ["@types/node@20.19.43", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA=="], "@modelcontextprotocol/sdk/@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="], @@ -1614,6 +1635,8 @@ "@mcpedia/db/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@mcpedia/mcp-client/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@mcpedia/mcp/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "@mcpedia/web/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],