Files
mcpedia/apps/mcp-client/src/ask.ts
T
asepharyana ec0abfbd60
CI / typecheck + build (turbo) (push) Canceled after 0s
feat(mcp-client): MCP client CLI for interacting with MCPedia server
New app: apps/mcp-client/ (bun workspace package)

Interactive REPL (bun run chat):
- Connects to MCP server via Streamable HTTP transport
- Tools: /tools, /resources, /search, /ss (semantic), /hybrid,
  /doc, /related, /create (interactive prompts), /update, /delete,
  /index, /queue_status, /help, /quit
- Sends x-webhook-secret header for write tools

One-shot CLI (bun run ask):
- ask <command> [args] — list tools, search, get doc, create, delete, etc.

API client wrapper (src/client.ts):
- McpediaClient class with typed methods for all 13 MCP tools + 5 resources
- StreamableHTTPClientTransport with custom headers (auth)
- Graceful disconnect

Tests (7, all green, no network/DB):
- listTools, getDocument, createDocument, deleteDocument, listResources,
  readResource, header-passing

Deps: @modelcontextprotocol/sdk ^1.29.0, zod ^4.0.0, github-slugger (for web TOC)
2026-08-20 17:57:58 +07:00

132 lines
4.2 KiB
TypeScript

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 <command> [args...]");
console.error("Commands: tools, resources, search <q>, ss <q>, hybrid <q>, doc <slug>, related <slug>, create, delete <slug>, index <slug>, status");
process.exit(1);
}
const headers: Record<string, string> = {};
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<string, string> = {};
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);
});