@yichus/mcp 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/cli.ts
4
+ import { parseArgs } from "util";
5
+ import { z as z3 } from "zod";
6
+
3
7
  // src/server.ts
4
- import { LocalRelayMcpServer } from "@mcp-b/webmcp-local-relay";
5
- import { z } from "zod";
8
+ import { z as z2 } from "zod";
6
9
 
7
10
  // src/calculators.ts
8
11
  var DEFAULT_BASE_URL = "https://yich.us";
@@ -19,6 +22,12 @@ var CALCULATORS = [
19
22
  description: "How much water does one rainfall event collect, and how much overflows the tank?",
20
23
  path: "demos/simulation/rainwater.html"
21
24
  },
25
+ {
26
+ slug: "rainwater-history",
27
+ title: "Rainwater through a dry spell",
28
+ description: "Run NOAA weather observations through an inspectable daily tank model. Change capacity and demand, inspect captured data, and share a saved report.",
29
+ path: "demos/simulation/rainwater-history.html"
30
+ },
22
31
  {
23
32
  slug: "battery-runtime",
24
33
  title: "Battery runtime",
@@ -168,44 +177,272 @@ function openUrlInBrowser(rawUrl) {
168
177
  command = "xdg-open";
169
178
  args = [validated];
170
179
  }
171
- spawn(command, args, { detached: true, stdio: "ignore" }).unref();
172
- return { command, args };
180
+ return new Promise((resolve) => {
181
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
182
+ child.once("spawn", () => {
183
+ child.unref();
184
+ resolve({ command, args, opened: true });
185
+ });
186
+ child.on("error", (error) => resolve({ command, args, opened: false, error: error.message }));
187
+ });
188
+ }
189
+
190
+ // src/relay.ts
191
+ import {
192
+ LocalRelayMcpServer,
193
+ RelayBridgeServer,
194
+ RelayRegistry,
195
+ buildPublicToolName
196
+ } from "@mcp-b/webmcp-local-relay";
197
+ var REMEMBERED_TABS = 256;
198
+ var FirstTabNamingRegistry = class extends RelayRegistry {
199
+ firstSeen = /* @__PURE__ */ new Map();
200
+ tabByConnection = /* @__PURE__ */ new Map();
201
+ seen = 0;
202
+ upsertSource(connectionId, hello) {
203
+ super.upsertSource(connectionId, hello);
204
+ this.tabByConnection.set(connectionId, hello.tabId);
205
+ this.remember(hello.tabId);
206
+ }
207
+ removeConnection(connectionId) {
208
+ super.removeConnection(connectionId);
209
+ this.tabByConnection.delete(connectionId);
210
+ }
211
+ listTools() {
212
+ return this.named().map(({ tool }) => tool).sort((a, b) => a.name.localeCompare(b.name));
213
+ }
214
+ resolveInvocation(options) {
215
+ const entry = this.named().find(({ tool }) => tool.name === options.toolName);
216
+ if (!entry) return null;
217
+ const resolved = super.resolveInvocation({ ...options, toolName: entry.relayName });
218
+ return resolved && { ...resolved, publicToolName: entry.tool.name };
219
+ }
220
+ // Each relay tool with the name an agent sees: the plain name for the
221
+ // copy published by the first-seen tab among those publishing it.
222
+ named() {
223
+ const tools = super.listTools();
224
+ const owners = /* @__PURE__ */ new Map();
225
+ for (const tool of tools) {
226
+ for (const source of tool.sources) {
227
+ const order = this.order(source.tabId);
228
+ owners.set(tool.originalName, Math.min(owners.get(tool.originalName) ?? order, order));
229
+ }
230
+ }
231
+ return tools.map((tool) => {
232
+ const owned = tool.sources.some((source) => this.order(source.tabId) === owners.get(tool.originalName));
233
+ const name = owned ? buildPublicToolName({ originalToolName: tool.originalName }) : tool.name;
234
+ return { relayName: tool.name, tool: { ...tool, name } };
235
+ });
236
+ }
237
+ order(tabId) {
238
+ return this.firstSeen.get(tabId) ?? Number.POSITIVE_INFINITY;
239
+ }
240
+ remember(tabId) {
241
+ if (this.firstSeen.has(tabId)) return;
242
+ this.firstSeen.set(tabId, this.seen++);
243
+ const open = new Set(this.tabByConnection.values());
244
+ for (const tab of this.firstSeen.keys()) {
245
+ if (this.firstSeen.size - open.size <= REMEMBERED_TABS) break;
246
+ if (!open.has(tab)) this.firstSeen.delete(tab);
247
+ }
248
+ }
249
+ };
250
+ var TOOL_LIST_CHANGED = "notifications/tools/list_changed";
251
+ var isToolListChanged = (message) => "method" in message && message.method === TOOL_LIST_CHANGED && !("id" in message);
252
+ var ToolListChangedCoalescer = class {
253
+ constructor(inner, windowMs) {
254
+ this.inner = inner;
255
+ this.windowMs = windowMs;
256
+ inner.onmessage = (message, extra) => this.onmessage?.(message, extra);
257
+ inner.onerror = (error) => this.onerror?.(error);
258
+ inner.onclose = () => {
259
+ this.drop();
260
+ this.onclose?.();
261
+ };
262
+ }
263
+ inner;
264
+ windowMs;
265
+ onclose;
266
+ onerror;
267
+ onmessage;
268
+ pending = null;
269
+ get sessionId() {
270
+ return this.inner.sessionId;
271
+ }
272
+ setProtocolVersion(version) {
273
+ this.inner.setProtocolVersion?.(version);
274
+ }
275
+ start() {
276
+ return this.inner.start();
277
+ }
278
+ async send(message, options) {
279
+ if (!isToolListChanged(message)) return this.inner.send(message, options);
280
+ if (this.pending) return;
281
+ this.pending = setTimeout(() => {
282
+ this.pending = null;
283
+ this.inner.send(message, options).catch((error) => this.onerror?.(error));
284
+ }, this.windowMs);
285
+ this.pending.unref?.();
286
+ }
287
+ close() {
288
+ this.drop();
289
+ return this.inner.close();
290
+ }
291
+ drop() {
292
+ if (this.pending) clearTimeout(this.pending);
293
+ this.pending = null;
294
+ }
295
+ };
296
+ var CoalescingRelayServer = class extends LocalRelayMcpServer {
297
+ constructor(options, windowMs) {
298
+ super(options);
299
+ this.windowMs = windowMs;
300
+ }
301
+ windowMs;
302
+ connect(transport) {
303
+ return super.connect(new ToolListChangedCoalescer(transport, this.windowMs));
304
+ }
305
+ };
306
+ var PageAnswerBridge = class extends RelayBridgeServer {
307
+ pageContent = /* @__PURE__ */ new WeakMap();
308
+ async invokeTool(...call) {
309
+ const result = await super.invokeTool(...call);
310
+ this.pageContent.set(result, result.content);
311
+ return result;
312
+ }
313
+ /** The content a page answered with, for an answer this bridge carried. */
314
+ pageAnswer(result) {
315
+ return this.pageContent.get(result);
316
+ }
317
+ };
318
+
319
+ // src/saveCalculator.ts
320
+ import { mkdir, writeFile } from "fs/promises";
321
+ import { isAbsolute, join } from "path";
322
+ import { z } from "zod";
323
+ var saveCalculatorInput = z.object({
324
+ exportToolName: z.string().regex(/^yichus_example_export(?:_[a-zA-Z0-9_-]+)?$/).describe("Public export tool name from webmcp_list_tools, matched to the intended tab."),
325
+ exampleId: z.string().min(1),
326
+ expectedRevision: z.number().int().nonnegative(),
327
+ outputDirectory: z.string().refine(isAbsolute, "Use an absolute output directory.").describe("A new directory under an existing parent. Existing directories are refused; files are never overwritten.")
328
+ });
329
+ var chunkSchema = z.object({
330
+ exampleId: z.string(),
331
+ revision: z.number().int(),
332
+ exportId: z.string().min(1),
333
+ assetBaseUrl: z.url(),
334
+ sources: z.array(z.object({ fileName: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*\.bosatsu$/), source: z.string().max(1e6) })).min(1).max(16),
335
+ html: z.object({
336
+ fileName: z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*\.html$/),
337
+ content: z.string().min(1).max(32768),
338
+ offset: z.number().int().nonnegative(),
339
+ totalLength: z.number().int().positive().max(32 * 1024 * 1024),
340
+ nextOffset: z.number().int().nonnegative().nullable()
341
+ })
342
+ });
343
+ async function saveCalculator(rawArgs, invoke) {
344
+ const args = saveCalculatorInput.parse(rawArgs);
345
+ const content = [];
346
+ let first;
347
+ let offset = 0;
348
+ for (; ; ) {
349
+ const response = z.object({ isError: z.boolean().optional(), structuredContent: z.unknown().optional() }).parse(await invoke(args.exportToolName, {
350
+ exampleId: args.exampleId,
351
+ expectedRevision: args.expectedRevision,
352
+ offset,
353
+ ...first ? { expectedExportId: first.exportId } : {}
354
+ }));
355
+ if (response.isError) throw new Error("Browser export failed. Read the current example feedback before retrying.");
356
+ const chunk = chunkSchema.parse(response.structuredContent);
357
+ if (chunk.exampleId !== args.exampleId || chunk.revision !== args.expectedRevision || chunk.html.offset !== offset) throw new Error("Export does not match the requested example, revision, or offset.");
358
+ if (first && (chunk.exportId !== first.exportId || chunk.assetBaseUrl !== first.assetBaseUrl || chunk.html.fileName !== first.html.fileName || chunk.html.totalLength !== first.html.totalLength || JSON.stringify(chunk.sources) !== JSON.stringify(first.sources))) throw new Error("Export changed during transfer. Nothing was saved; start again.");
359
+ first ??= chunk;
360
+ const end = offset + chunk.html.content.length;
361
+ if (end > chunk.html.totalLength || (chunk.html.nextOffset === null ? end !== chunk.html.totalLength : chunk.html.nextOffset !== end || end >= chunk.html.totalLength)) throw new Error("Incomplete or inconsistent export chunk. Nothing was saved.");
362
+ content.push(chunk.html.content);
363
+ if (chunk.html.nextOffset === null) break;
364
+ offset = chunk.html.nextOffset;
365
+ }
366
+ const files = [...first.sources.map((s) => ({ fileName: s.fileName, content: s.source })), { fileName: first.html.fileName, content: content.join("") }];
367
+ if (new Set(files.map((f) => f.fileName.toLowerCase())).size !== files.length) throw new Error("Export file names must be unique.");
368
+ await mkdir(args.outputDirectory);
369
+ for (const file of files) await writeFile(join(args.outputDirectory, file.fileName), file.content, { encoding: "utf8", flag: "wx" });
370
+ return {
371
+ ok: true,
372
+ directory: args.outputDirectory,
373
+ revision: first.revision,
374
+ exportId: first.exportId,
375
+ assetBaseUrl: first.assetBaseUrl,
376
+ files: files.map((f) => ({ fileName: f.fileName, path: join(args.outputDirectory, f.fileName), bytes: Buffer.byteLength(f.content) })),
377
+ htmlPath: join(args.outputDirectory, first.html.fileName),
378
+ note: "The HTML starts from configured defaults and loads supporting assets from the site. It requires network access. No program was executed."
379
+ };
173
380
  }
174
381
 
175
382
  // src/server.ts
176
383
  var PACKAGE_NAME = "yichus-mcp";
177
- var PACKAGE_VERSION = "0.1.0";
384
+ var PACKAGE_VERSION = "0.1.1";
178
385
  function createYichusMcpServer(options = {}) {
179
386
  const baseUrl = normalizeBaseUrl(options.baseUrl ?? "https://yich.us");
180
387
  const bridgeOverrides = options.bridgeOverrides ?? {};
181
- const server = new LocalRelayMcpServer({
388
+ const bridge = new PageAnswerBridge({
389
+ host: bridgeOverrides.host,
390
+ port: bridgeOverrides.port,
391
+ label: bridgeOverrides.label ?? "Yichus",
392
+ // The pair every page hints the widget (web/webmcp-autocomplete.js,
393
+ // the generated calculators); the widget rejects any other hello.
394
+ workspace: bridgeOverrides.workspace ?? "yichus",
395
+ relayId: bridgeOverrides.relayId ?? "yichus",
396
+ allowedOrigins: [baseUrl],
397
+ // Cold browser compilation can outlast the relay's 25-second default.
398
+ // Keep a deadline so an unresponsive tab still fails explicitly.
399
+ invokeTimeoutMs: 12e4
400
+ }, new FirstTabNamingRegistry());
401
+ const server = new CoalescingRelayServer({
182
402
  serverName: PACKAGE_NAME,
183
403
  serverVersion: PACKAGE_VERSION,
184
- bridgeOptions: {
185
- host: bridgeOverrides.host,
186
- port: bridgeOverrides.port,
187
- label: bridgeOverrides.label ?? "Yichus",
188
- // The pair every page hints the widget (web/webmcp-autocomplete.js,
189
- // the generated calculators); the widget rejects any other hello.
190
- workspace: bridgeOverrides.workspace ?? "yichus",
191
- relayId: bridgeOverrides.relayId ?? "yichus",
192
- allowedOrigins: [baseUrl]
193
- }
194
- });
404
+ bridge
405
+ }, TOOL_LIST_CHANGED_WINDOW_MS);
195
406
  registerYichusTools(server, { baseUrl, disableOpener: options.disableOpener ?? false });
407
+ answerRefusalsAsThePageWroteThem(server, bridge);
196
408
  return server;
197
409
  }
198
- function asInternal(server) {
199
- return server;
200
- }
410
+ var TOOL_LIST_CHANGED_WINDOW_MS = 100;
411
+ var isFunction = (field) => typeof field === "function";
412
+ var StaticToolFields = z2.object({
413
+ mcpServer: z2.object({
414
+ registerTool: z2.custom(isFunction),
415
+ // The MCP SDK's record of each registered tool; the relay's
416
+ // webmcp_call_tool is wrapped there (answerRefusalsAsThePageWroteThem).
417
+ _registeredTools: z2.object({ webmcp_call_tool: z2.object({ handler: z2.custom(isFunction) }) })
418
+ }),
419
+ staticToolData: z2.instanceof(Map)
420
+ });
421
+ var StaticToolHost = z2.custom(
422
+ (value) => StaticToolFields.safeParse(value).success,
423
+ { error: "@mcp-b/webmcp-local-relay no longer has mcpServer.registerTool, a registered webmcp_call_tool handler and staticToolData" }
424
+ );
201
425
  function registerYichusTools(server, ctx) {
202
- const internal = asInternal(server);
203
- registerStatic(internal, {
426
+ const host = StaticToolHost.parse(server);
427
+ registerStatic(host, {
428
+ name: "yichus_save_calculator",
429
+ description: "Save a browser-compiled example and its Bosatsu sources to a NEW local directory. Pass the public yichus_example_export tool name for the intended tab and its current source revision. Transfers bounded chunks internally and returns file paths, not generated HTML. Does not execute the program. Requires the browser tab to stay open.",
430
+ annotations: { readOnlyHint: false, destructiveHint: false },
431
+ input: saveCalculatorInput,
432
+ handler: async (args) => {
433
+ try {
434
+ const payload = await saveCalculator(args, (name, params) => server.bridge.invokeTool(name, params));
435
+ return { content: [{ type: "text", text: JSON.stringify(payload) }], structuredContent: payload };
436
+ } catch (error) {
437
+ return { isError: true, content: [{ type: "text", text: String(error) }] };
438
+ }
439
+ }
440
+ });
441
+ registerStatic(host, {
204
442
  name: "yichus_list_calculators",
205
443
  description: "List the curated Yichus calculator demos and the playground editor available on the configured Yichus origin. Returns a slug, title, description, and absolute URL for each.",
206
444
  annotations: { readOnlyHint: true, idempotentHint: true },
207
- inputSchemaJson: { type: "object", properties: {}, additionalProperties: false },
208
- inputSchemaZod: {},
445
+ input: z2.object({}),
209
446
  handler: async () => {
210
447
  const entries = listCalculators().map((calc) => ({
211
448
  slug: calc.slug,
@@ -220,24 +457,16 @@ function registerYichusTools(server, ctx) {
220
457
  };
221
458
  }
222
459
  });
223
- registerStatic(internal, {
460
+ registerStatic(host, {
224
461
  name: "yichus_open_calculator",
225
462
  description: "Open a curated Yichus calculator (or the playground) in the user's default browser with WebMCP enabled (?webmcp=1). After opening, the relay tools below will see the page's tools (yichus_list_inputs, yichus_set_inputs, yichus_list_outputs, yichus_explain_output for calculators; yichus_playground_* for the playground). Use yichus_list_calculators to discover slugs.",
226
463
  annotations: { openWorldHint: true },
227
- inputSchemaJson: {
228
- type: "object",
229
- properties: {
230
- slug: {
231
- type: "string",
232
- description: 'Calculator slug from yichus_list_calculators (for example "loan", "investment", "tax", "playground").'
233
- }
234
- },
235
- required: ["slug"],
236
- additionalProperties: false
237
- },
238
- inputSchemaZod: { slug: z.string() },
239
- handler: async (rawArgs) => {
240
- const args = z.object({ slug: z.string() }).parse(rawArgs);
464
+ input: z2.object({
465
+ slug: z2.string().describe(
466
+ 'Calculator slug from yichus_list_calculators (for example "loan", "investment", "tax", "playground").'
467
+ )
468
+ }),
469
+ handler: async (args) => {
241
470
  const calc = findCalculator(args.slug);
242
471
  if (!calc) {
243
472
  const known = listCalculators().map((c) => c.slug).join(", ");
@@ -252,10 +481,7 @@ function registerYichusTools(server, ctx) {
252
481
  };
253
482
  }
254
483
  const url = withWebMcpFlag(calculatorUrl(calc, ctx.baseUrl));
255
- let opener = null;
256
- if (!ctx.disableOpener) {
257
- opener = openUrlInBrowser(url);
258
- }
484
+ const attempt = ctx.disableOpener ? null : await openUrlInBrowser(url);
259
485
  const payload = {
260
486
  calculator: {
261
487
  slug: calc.slug,
@@ -263,35 +489,39 @@ function registerYichusTools(server, ctx) {
263
489
  description: calc.description
264
490
  },
265
491
  url,
266
- opened: !ctx.disableOpener,
267
- opener
492
+ opened: attempt?.opened ?? false,
493
+ opener: attempt && { command: attempt.command, args: attempt.args },
494
+ ...attempt?.error === void 0 ? {} : { openerError: attempt.error }
268
495
  };
269
496
  return {
270
- content: [
271
- {
272
- type: "text",
273
- text: ctx.disableOpener ? `Would open ${calc.title} at ${url} (browser opener disabled).` : `Opening ${calc.title} at ${url}.`
274
- }
275
- ],
497
+ content: [{ type: "text", text: openAnswer(calc, url, attempt) }],
276
498
  structuredContent: payload
277
499
  };
278
500
  }
279
501
  });
280
502
  }
281
- function registerStatic(internal, spec) {
282
- const config = {
283
- description: spec.description,
284
- inputSchema: spec.inputSchemaZod
503
+ function openAnswer(calc, url, attempt) {
504
+ if (attempt === null) return `Would open ${calc.title} at ${url} (browser opener disabled).`;
505
+ if (attempt.opened) return `Opening ${calc.title} at ${url}.`;
506
+ return `Could not open a browser on this machine (${attempt.error}). Open this link in a browser yourself to load ${calc.title}: ${url}`;
507
+ }
508
+ function answerRefusalsAsThePageWroteThem(server, bridge) {
509
+ const callTool = StaticToolHost.parse(server).mcpServer._registeredTools.webmcp_call_tool;
510
+ const relayHandler = callTool.handler;
511
+ callTool.handler = async (...call) => {
512
+ const result = await relayHandler(...call);
513
+ const page = bridge.pageAnswer(result);
514
+ return result.isError === true && page !== void 0 ? { ...result, content: page } : result;
285
515
  };
286
- if (spec.annotations && Object.keys(spec.annotations).length > 0) {
287
- config.annotations = spec.annotations;
288
- }
289
- internal.mcpServer.registerTool(spec.name, config, spec.handler);
290
- internal.staticToolData.set(spec.name, {
291
- name: spec.name,
292
- description: spec.description,
293
- inputSchema: spec.inputSchemaJson,
294
- ...spec.annotations && Object.keys(spec.annotations).length > 0 ? { annotations: spec.annotations } : {}
516
+ }
517
+ function registerStatic(host, spec) {
518
+ const { name, description, annotations, input, handler } = spec;
519
+ host.mcpServer.registerTool(name, { description, inputSchema: input.shape, annotations }, handler);
520
+ host.staticToolData.set(name, {
521
+ name,
522
+ description,
523
+ inputSchema: z2.toJSONSchema(input, { unrepresentable: "any" }),
524
+ annotations
295
525
  });
296
526
  }
297
527
  function normalizeBaseUrl(raw) {
@@ -299,59 +529,37 @@ function normalizeBaseUrl(raw) {
299
529
  }
300
530
 
301
531
  // src/cli.ts
532
+ var Port = z3.coerce.number().int().min(1).max(65535);
302
533
  function parseFlags(argv) {
303
- const flags = {};
304
- for (let i = 0; i < argv.length; i++) {
305
- const arg = argv[i];
306
- const next = () => {
307
- const value = argv[i + 1];
308
- if (value === void 0) {
309
- throw new Error(`Missing value for ${arg}`);
310
- }
311
- i += 1;
312
- return value;
313
- };
314
- switch (arg) {
315
- case "--base-url":
316
- flags.baseUrl = next();
317
- break;
318
- case "--host":
319
- flags.host = next();
320
- break;
321
- case "--port": {
322
- const value = next();
323
- const parsed = Number.parseInt(value, 10);
324
- if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
325
- throw new Error(`Invalid --port value: ${value}`);
326
- }
327
- flags.port = parsed;
328
- break;
329
- }
330
- case "--label":
331
- flags.label = next();
332
- break;
333
- case "--workspace":
334
- flags.workspace = next();
335
- break;
336
- case "--relay-id":
337
- flags.relayId = next();
338
- break;
339
- case "--help":
340
- case "-h":
341
- printHelp();
342
- process.exit(0);
343
- // eslint-disable-next-line no-fallthrough
344
- default:
345
- throw new Error(`Unknown flag: ${arg}`);
346
- }
534
+ const { values } = parseArgs({ args: argv, strict: true, options: {
535
+ "base-url": { type: "string" },
536
+ host: { type: "string" },
537
+ port: { type: "string" },
538
+ label: { type: "string" },
539
+ workspace: { type: "string" },
540
+ "relay-id": { type: "string" },
541
+ help: { type: "boolean", short: "h" }
542
+ } });
543
+ if (values.help) {
544
+ printHelp();
545
+ process.exit(0);
347
546
  }
348
- return flags;
547
+ const port = values.port === void 0 ? void 0 : Port.safeParse(values.port);
548
+ if (port && !port.success) throw new Error(`Invalid --port value: ${values.port}`);
549
+ return {
550
+ baseUrl: values["base-url"],
551
+ host: values.host,
552
+ port: port?.data,
553
+ label: values.label,
554
+ workspace: values.workspace,
555
+ relayId: values["relay-id"]
556
+ };
349
557
  }
350
558
  function printHelp() {
351
559
  process.stdout.write(`yichus-mcp [options]
352
560
 
353
- Yichus MCP server: bundles the WebMCP local relay with two extra static
354
- tools (yichus_list_calculators, yichus_open_calculator) so an MCP client
561
+ Yichus MCP server: bundles the WebMCP local relay with three static tools
562
+ (yichus_list_calculators, yichus_open_calculator, yichus_save_calculator) so an MCP client
355
563
  can drive any deployed Yichus calculator end to end.
356
564
 
357
565
  Options:
@@ -369,13 +577,14 @@ Options:
369
577
  built to hint the same values.
370
578
  --help, -h Show this help.
371
579
 
372
- Add to your MCP client config (Claude Desktop, Cursor, etc.):
580
+ Build from the checkout: pnpm install && pnpm run yichus-mcp:build
581
+ Then add to your MCP client config (replace the absolute checkout path):
373
582
 
374
583
  {
375
584
  "mcpServers": {
376
585
  "yichus": {
377
- "command": "npx",
378
- "args": ["-y", "@yichus/mcp"]
586
+ "command": "node",
587
+ "args": ["/absolute/path/to/yichus/packages/yichus-mcp/dist/cli.js"]
379
588
  }
380
589
  }
381
590
  }