@yichus/mcp 0.1.0 → 0.1.2

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,12 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/cli.ts
4
+ import { parseArgs } from "util";
5
+ import { z as z4 } 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 z3 } from "zod";
6
9
 
7
10
  // src/calculators.ts
8
- var DEFAULT_BASE_URL = "https://yich.us";
9
- var CALCULATORS = [
11
+ import { z } from "zod";
12
+
13
+ // src/calculators.json
14
+ var calculators_default = [
10
15
  {
11
16
  slug: "event-budget",
12
17
  title: "Event budget",
@@ -19,6 +24,12 @@ var CALCULATORS = [
19
24
  description: "How much water does one rainfall event collect, and how much overflows the tank?",
20
25
  path: "demos/simulation/rainwater.html"
21
26
  },
27
+ {
28
+ slug: "rainwater-history",
29
+ title: "Rainwater through a dry spell",
30
+ description: "Run NOAA weather observations, or your own daily rainfall CSV, through an inspectable daily tank model. Change capacity and demand, inspect captured data, and share a saved report.",
31
+ path: "demos/simulation/rainwater-history.html"
32
+ },
22
33
  {
23
34
  slug: "battery-runtime",
24
35
  title: "Battery runtime",
@@ -128,6 +139,16 @@ var CALCULATORS = [
128
139
  path: "demos/simulation/carbon-abatement.html"
129
140
  }
130
141
  ];
142
+
143
+ // src/calculators.ts
144
+ var CalculatorEntry = z.strictObject({
145
+ slug: z.string(),
146
+ title: z.string(),
147
+ description: z.string(),
148
+ path: z.string()
149
+ });
150
+ var DEFAULT_BASE_URL = "https://yich.us";
151
+ var CALCULATORS = z.array(CalculatorEntry).parse(calculators_default);
131
152
  var PLAYGROUND = {
132
153
  slug: "playground",
133
154
  title: "Yichus Bosatsu Playground",
@@ -168,44 +189,281 @@ function openUrlInBrowser(rawUrl) {
168
189
  command = "xdg-open";
169
190
  args = [validated];
170
191
  }
171
- spawn(command, args, { detached: true, stdio: "ignore" }).unref();
172
- return { command, args };
192
+ return new Promise((resolve) => {
193
+ const child = spawn(command, args, { detached: true, stdio: "ignore" });
194
+ child.once("spawn", () => {
195
+ child.unref();
196
+ resolve({ command, args, opened: true });
197
+ });
198
+ child.on("error", (error) => resolve({ command, args, opened: false, error: error.message }));
199
+ });
200
+ }
201
+
202
+ // src/relay.ts
203
+ import {
204
+ LocalRelayMcpServer,
205
+ RelayBridgeServer,
206
+ RelayRegistry,
207
+ buildPublicToolName
208
+ } from "@mcp-b/webmcp-local-relay";
209
+ var REMEMBERED_TABS = 256;
210
+ var FirstTabNamingRegistry = class extends RelayRegistry {
211
+ firstSeen = /* @__PURE__ */ new Map();
212
+ tabByConnection = /* @__PURE__ */ new Map();
213
+ seen = 0;
214
+ upsertSource(connectionId, hello) {
215
+ super.upsertSource(connectionId, hello);
216
+ this.tabByConnection.set(connectionId, hello.tabId);
217
+ this.remember(hello.tabId);
218
+ }
219
+ removeConnection(connectionId) {
220
+ super.removeConnection(connectionId);
221
+ this.tabByConnection.delete(connectionId);
222
+ }
223
+ listTools() {
224
+ return this.named().map(({ tool }) => tool).sort((a, b) => a.name.localeCompare(b.name));
225
+ }
226
+ resolveInvocation(options) {
227
+ const entry = this.named().find(({ tool }) => tool.name === options.toolName);
228
+ if (!entry) return null;
229
+ const resolved = super.resolveInvocation({ ...options, toolName: entry.relayName });
230
+ return resolved && { ...resolved, publicToolName: entry.tool.name };
231
+ }
232
+ // Each relay tool with the name an agent sees: the plain name for the
233
+ // copy published by the first-seen tab among those publishing it.
234
+ named() {
235
+ const tools = super.listTools();
236
+ const owners = /* @__PURE__ */ new Map();
237
+ for (const tool of tools) {
238
+ for (const source of tool.sources) {
239
+ const order = this.order(source.tabId);
240
+ owners.set(tool.originalName, Math.min(owners.get(tool.originalName) ?? order, order));
241
+ }
242
+ }
243
+ return tools.map((tool) => {
244
+ const owned = tool.sources.some((source) => this.order(source.tabId) === owners.get(tool.originalName));
245
+ const name = owned ? buildPublicToolName({ originalToolName: tool.originalName }) : tool.name;
246
+ return { relayName: tool.name, tool: { ...tool, name } };
247
+ });
248
+ }
249
+ order(tabId) {
250
+ return this.firstSeen.get(tabId) ?? Number.POSITIVE_INFINITY;
251
+ }
252
+ remember(tabId) {
253
+ if (this.firstSeen.has(tabId)) return;
254
+ this.firstSeen.set(tabId, this.seen++);
255
+ const open = new Set(this.tabByConnection.values());
256
+ for (const tab of this.firstSeen.keys()) {
257
+ if (this.firstSeen.size - open.size <= REMEMBERED_TABS) break;
258
+ if (!open.has(tab)) this.firstSeen.delete(tab);
259
+ }
260
+ }
261
+ };
262
+ var TOOL_LIST_CHANGED = "notifications/tools/list_changed";
263
+ var isToolListChanged = (message) => "method" in message && message.method === TOOL_LIST_CHANGED && !("id" in message);
264
+ var ToolListChangedCoalescer = class {
265
+ constructor(inner, windowMs) {
266
+ this.inner = inner;
267
+ this.windowMs = windowMs;
268
+ inner.onmessage = (message, extra) => this.onmessage?.(message, extra);
269
+ inner.onerror = (error) => this.onerror?.(error);
270
+ inner.onclose = () => {
271
+ this.drop();
272
+ this.onclose?.();
273
+ };
274
+ }
275
+ inner;
276
+ windowMs;
277
+ onclose;
278
+ onerror;
279
+ onmessage;
280
+ pending = null;
281
+ get sessionId() {
282
+ return this.inner.sessionId;
283
+ }
284
+ setProtocolVersion(version) {
285
+ this.inner.setProtocolVersion?.(version);
286
+ }
287
+ start() {
288
+ return this.inner.start();
289
+ }
290
+ async send(message, options) {
291
+ if (!isToolListChanged(message)) return this.inner.send(message, options);
292
+ if (this.pending) return;
293
+ this.pending = setTimeout(() => {
294
+ this.pending = null;
295
+ this.inner.send(message, options).catch((error) => this.onerror?.(error));
296
+ }, this.windowMs);
297
+ this.pending.unref?.();
298
+ }
299
+ close() {
300
+ this.drop();
301
+ return this.inner.close();
302
+ }
303
+ drop() {
304
+ if (this.pending) clearTimeout(this.pending);
305
+ this.pending = null;
306
+ }
307
+ };
308
+ var CoalescingRelayServer = class extends LocalRelayMcpServer {
309
+ constructor(options, windowMs) {
310
+ super(options);
311
+ this.windowMs = windowMs;
312
+ }
313
+ windowMs;
314
+ connect(transport) {
315
+ return super.connect(new ToolListChangedCoalescer(transport, this.windowMs));
316
+ }
317
+ };
318
+ var PageAnswerBridge = class extends RelayBridgeServer {
319
+ pageContent = /* @__PURE__ */ new WeakMap();
320
+ async invokeTool(...call) {
321
+ const result = await super.invokeTool(...call);
322
+ this.pageContent.set(result, result.content);
323
+ return result;
324
+ }
325
+ /** The content a page answered with, for an answer this bridge carried. */
326
+ pageAnswer(result) {
327
+ return this.pageContent.get(result);
328
+ }
329
+ };
330
+
331
+ // src/saveCalculator.ts
332
+ import { mkdir, writeFile } from "fs/promises";
333
+ import { isAbsolute, join } from "path";
334
+ import { z as z2 } from "zod";
335
+ var saveCalculatorInput = z2.object({
336
+ exportToolName: z2.string().regex(/^yichus_example_export(?:_[a-zA-Z0-9_-]+)?$/).describe("Public export tool name from webmcp_list_tools, matched to the intended tab."),
337
+ exampleId: z2.string().min(1),
338
+ expectedRevision: z2.number().int().nonnegative(),
339
+ outputDirectory: z2.string().refine(isAbsolute, "Use an absolute output directory.").describe("A new directory under an existing parent. Existing directories are refused; files are never overwritten.")
340
+ });
341
+ var chunkSchema = z2.object({
342
+ exampleId: z2.string(),
343
+ revision: z2.number().int(),
344
+ exportId: z2.string().min(1),
345
+ assetBaseUrl: z2.url(),
346
+ selfContained: z2.boolean().default(false),
347
+ sources: z2.array(z2.object({ fileName: z2.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*\.bosatsu$/), source: z2.string().max(1e6) })).min(1).max(16),
348
+ html: z2.object({
349
+ fileName: z2.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9._-]*\.html$/),
350
+ content: z2.string().min(1).max(32768),
351
+ offset: z2.number().int().nonnegative(),
352
+ totalLength: z2.number().int().positive().max(32 * 1024 * 1024),
353
+ nextOffset: z2.number().int().nonnegative().nullable()
354
+ })
355
+ });
356
+ async function saveCalculator(rawArgs, invoke) {
357
+ const args = saveCalculatorInput.parse(rawArgs);
358
+ const content = [];
359
+ let first;
360
+ let offset = 0;
361
+ for (; ; ) {
362
+ const response = z2.object({ isError: z2.boolean().optional(), structuredContent: z2.unknown().optional() }).parse(await invoke(args.exportToolName, {
363
+ exampleId: args.exampleId,
364
+ expectedRevision: args.expectedRevision,
365
+ offset,
366
+ ...first ? { expectedExportId: first.exportId } : {}
367
+ }));
368
+ if (response.isError) throw new Error("Browser export failed. Read the current example feedback before retrying.");
369
+ const chunk = chunkSchema.parse(response.structuredContent);
370
+ 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.");
371
+ if (first && (chunk.exportId !== first.exportId || chunk.assetBaseUrl !== first.assetBaseUrl || chunk.selfContained !== first.selfContained || 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.");
372
+ first ??= chunk;
373
+ const end = offset + chunk.html.content.length;
374
+ 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.");
375
+ content.push(chunk.html.content);
376
+ if (chunk.html.nextOffset === null) break;
377
+ offset = chunk.html.nextOffset;
378
+ }
379
+ const files = [...first.sources.map((s) => ({ fileName: s.fileName, content: s.source })), { fileName: first.html.fileName, content: content.join("") }];
380
+ if (new Set(files.map((f) => f.fileName.toLowerCase())).size !== files.length) throw new Error("Export file names must be unique.");
381
+ await mkdir(args.outputDirectory);
382
+ for (const file of files) await writeFile(join(args.outputDirectory, file.fileName), file.content, { encoding: "utf8", flag: "wx" });
383
+ return {
384
+ ok: true,
385
+ directory: args.outputDirectory,
386
+ revision: first.revision,
387
+ exportId: first.exportId,
388
+ ...first.selfContained ? {} : { assetBaseUrl: first.assetBaseUrl },
389
+ files: files.map((f) => ({ fileName: f.fileName, path: join(args.outputDirectory, f.fileName), bytes: Buffer.byteLength(f.content) })),
390
+ htmlPath: join(args.outputDirectory, first.html.fileName),
391
+ note: first.selfContained ? "The HTML starts from configured defaults and carries its own scripts and styles, so it can be hosted anywhere and calls no Yichus server. Linking an input to a formula loads the formula compiler from jsDelivr on first use. No program was executed." : "The HTML starts from configured defaults and loads supporting assets from the site. It requires network access. No program was executed."
392
+ };
173
393
  }
174
394
 
175
395
  // src/server.ts
176
396
  var PACKAGE_NAME = "yichus-mcp";
177
- var PACKAGE_VERSION = "0.1.0";
397
+ var PACKAGE_VERSION = "0.1.2";
178
398
  function createYichusMcpServer(options = {}) {
179
399
  const baseUrl = normalizeBaseUrl(options.baseUrl ?? "https://yich.us");
180
400
  const bridgeOverrides = options.bridgeOverrides ?? {};
181
- const server = new LocalRelayMcpServer({
401
+ const bridge = new PageAnswerBridge({
402
+ host: bridgeOverrides.host,
403
+ port: bridgeOverrides.port,
404
+ label: bridgeOverrides.label ?? "Yichus",
405
+ // The pair every page hints the widget (web/webmcp-autocomplete.js,
406
+ // the generated calculators); the widget rejects any other hello.
407
+ workspace: bridgeOverrides.workspace ?? "yichus",
408
+ relayId: bridgeOverrides.relayId ?? "yichus",
409
+ allowedOrigins: [baseUrl],
410
+ // Cold browser compilation can outlast the relay's 25-second default.
411
+ // Keep a deadline so an unresponsive tab still fails explicitly.
412
+ invokeTimeoutMs: 12e4
413
+ }, new FirstTabNamingRegistry());
414
+ const server = new CoalescingRelayServer({
182
415
  serverName: PACKAGE_NAME,
183
416
  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
- });
417
+ bridge
418
+ }, TOOL_LIST_CHANGED_WINDOW_MS);
195
419
  registerYichusTools(server, { baseUrl, disableOpener: options.disableOpener ?? false });
420
+ answerRefusalsAsThePageWroteThem(server, bridge);
196
421
  return server;
197
422
  }
198
- function asInternal(server) {
199
- return server;
200
- }
423
+ var relayInstructions = (baseUrl) => `Yichus in the browser: the tools here come from ${new URL(baseUrl).host} pages open in the person's browser, such as a calculator, the playground or the workbench. yichus_list_calculators and yichus_open_calculator open one, webmcp_list_tools lists what the open tabs offer, and each page's tools say what they do. Use them to explore a page with the person and explain what it shows.
424
+
425
+ To build the person's own app with them, use the Yichus studio instead. It is its own MCP server, run in their project folder (npx -y yichus@0.3.1 studio). It asks the person the product questions, checks the program after every change, and keeps a page open where you can point at what you mean while you talk.`;
426
+ var TOOL_LIST_CHANGED_WINDOW_MS = 100;
427
+ var isFunction = (field) => typeof field === "function";
428
+ var StaticToolFields = z3.object({
429
+ mcpServer: z3.object({
430
+ // The MCP SDK's Server keeps the handshake instructions here; the relay
431
+ // takes none of its own. server.test reads them back from initialize, so
432
+ // a release that moves them fails there.
433
+ server: z3.custom((value) => typeof value === "object" && value !== null && "_instructions" in value),
434
+ registerTool: z3.custom(isFunction),
435
+ // The MCP SDK's record of each registered tool; the relay's
436
+ // webmcp_call_tool is wrapped there (answerRefusalsAsThePageWroteThem).
437
+ _registeredTools: z3.object({ webmcp_call_tool: z3.object({ handler: z3.custom(isFunction) }) })
438
+ }),
439
+ staticToolData: z3.instanceof(Map)
440
+ });
441
+ var StaticToolHost = z3.custom(
442
+ (value) => StaticToolFields.safeParse(value).success,
443
+ { error: "@mcp-b/webmcp-local-relay no longer has mcpServer.registerTool, a registered webmcp_call_tool handler, an SDK server with instructions and staticToolData" }
444
+ );
201
445
  function registerYichusTools(server, ctx) {
202
- const internal = asInternal(server);
203
- registerStatic(internal, {
446
+ const host = StaticToolHost.parse(server);
447
+ host.mcpServer.server._instructions = relayInstructions(ctx.baseUrl);
448
+ registerStatic(host, {
449
+ name: "yichus_save_calculator",
450
+ 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.",
451
+ annotations: { readOnlyHint: false, destructiveHint: false },
452
+ input: saveCalculatorInput,
453
+ handler: async (args) => {
454
+ try {
455
+ const payload = await saveCalculator(args, (name, params) => server.bridge.invokeTool(name, params));
456
+ return { content: [{ type: "text", text: JSON.stringify(payload) }], structuredContent: payload };
457
+ } catch (error) {
458
+ return { isError: true, content: [{ type: "text", text: String(error) }] };
459
+ }
460
+ }
461
+ });
462
+ registerStatic(host, {
204
463
  name: "yichus_list_calculators",
205
464
  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
465
  annotations: { readOnlyHint: true, idempotentHint: true },
207
- inputSchemaJson: { type: "object", properties: {}, additionalProperties: false },
208
- inputSchemaZod: {},
466
+ input: z3.object({}),
209
467
  handler: async () => {
210
468
  const entries = listCalculators().map((calc) => ({
211
469
  slug: calc.slug,
@@ -220,24 +478,16 @@ function registerYichusTools(server, ctx) {
220
478
  };
221
479
  }
222
480
  });
223
- registerStatic(internal, {
481
+ registerStatic(host, {
224
482
  name: "yichus_open_calculator",
225
483
  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
484
  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);
485
+ input: z3.object({
486
+ slug: z3.string().describe(
487
+ 'Calculator slug from yichus_list_calculators (for example "loan", "investment", "tax", "playground").'
488
+ )
489
+ }),
490
+ handler: async (args) => {
241
491
  const calc = findCalculator(args.slug);
242
492
  if (!calc) {
243
493
  const known = listCalculators().map((c) => c.slug).join(", ");
@@ -252,10 +502,7 @@ function registerYichusTools(server, ctx) {
252
502
  };
253
503
  }
254
504
  const url = withWebMcpFlag(calculatorUrl(calc, ctx.baseUrl));
255
- let opener = null;
256
- if (!ctx.disableOpener) {
257
- opener = openUrlInBrowser(url);
258
- }
505
+ const attempt = ctx.disableOpener ? null : await openUrlInBrowser(url);
259
506
  const payload = {
260
507
  calculator: {
261
508
  slug: calc.slug,
@@ -263,35 +510,39 @@ function registerYichusTools(server, ctx) {
263
510
  description: calc.description
264
511
  },
265
512
  url,
266
- opened: !ctx.disableOpener,
267
- opener
513
+ opened: attempt?.opened ?? false,
514
+ opener: attempt && { command: attempt.command, args: attempt.args },
515
+ ...attempt?.error === void 0 ? {} : { openerError: attempt.error }
268
516
  };
269
517
  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
- ],
518
+ content: [{ type: "text", text: openAnswer(calc, url, attempt) }],
276
519
  structuredContent: payload
277
520
  };
278
521
  }
279
522
  });
280
523
  }
281
- function registerStatic(internal, spec) {
282
- const config = {
283
- description: spec.description,
284
- inputSchema: spec.inputSchemaZod
524
+ function openAnswer(calc, url, attempt) {
525
+ if (attempt === null) return `Would open ${calc.title} at ${url} (browser opener disabled).`;
526
+ if (attempt.opened) return `Opening ${calc.title} at ${url}.`;
527
+ return `Could not open a browser on this machine (${attempt.error}). Open this link in a browser yourself to load ${calc.title}: ${url}`;
528
+ }
529
+ function answerRefusalsAsThePageWroteThem(server, bridge) {
530
+ const callTool = StaticToolHost.parse(server).mcpServer._registeredTools.webmcp_call_tool;
531
+ const relayHandler = callTool.handler;
532
+ callTool.handler = async (...call) => {
533
+ const result = await relayHandler(...call);
534
+ const page = bridge.pageAnswer(result);
535
+ return result.isError === true && page !== void 0 ? { ...result, content: page } : result;
285
536
  };
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 } : {}
537
+ }
538
+ function registerStatic(host, spec) {
539
+ const { name, description, annotations, input, handler } = spec;
540
+ host.mcpServer.registerTool(name, { description, inputSchema: input.shape, annotations }, handler);
541
+ host.staticToolData.set(name, {
542
+ name,
543
+ description,
544
+ inputSchema: z3.toJSONSchema(input, { unrepresentable: "any" }),
545
+ annotations
295
546
  });
296
547
  }
297
548
  function normalizeBaseUrl(raw) {
@@ -299,59 +550,37 @@ function normalizeBaseUrl(raw) {
299
550
  }
300
551
 
301
552
  // src/cli.ts
553
+ var Port = z4.coerce.number().int().min(1).max(65535);
302
554
  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
- }
555
+ const { values } = parseArgs({ args: argv, strict: true, options: {
556
+ "base-url": { type: "string" },
557
+ host: { type: "string" },
558
+ port: { type: "string" },
559
+ label: { type: "string" },
560
+ workspace: { type: "string" },
561
+ "relay-id": { type: "string" },
562
+ help: { type: "boolean", short: "h" }
563
+ } });
564
+ if (values.help) {
565
+ printHelp();
566
+ process.exit(0);
347
567
  }
348
- return flags;
568
+ const port = values.port === void 0 ? void 0 : Port.safeParse(values.port);
569
+ if (port && !port.success) throw new Error(`Invalid --port value: ${values.port}`);
570
+ return {
571
+ baseUrl: values["base-url"],
572
+ host: values.host,
573
+ port: port?.data,
574
+ label: values.label,
575
+ workspace: values.workspace,
576
+ relayId: values["relay-id"]
577
+ };
349
578
  }
350
579
  function printHelp() {
351
580
  process.stdout.write(`yichus-mcp [options]
352
581
 
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
582
+ Yichus MCP server: bundles the WebMCP local relay with three static tools
583
+ (yichus_list_calculators, yichus_open_calculator, yichus_save_calculator) so an MCP client
355
584
  can drive any deployed Yichus calculator end to end.
356
585
 
357
586
  Options:
@@ -369,13 +598,13 @@ Options:
369
598
  built to hint the same values.
370
599
  --help, -h Show this help.
371
600
 
372
- Add to your MCP client config (Claude Desktop, Cursor, etc.):
601
+ Add to your MCP client config:
373
602
 
374
603
  {
375
604
  "mcpServers": {
376
605
  "yichus": {
377
606
  "command": "npx",
378
- "args": ["-y", "@yichus/mcp"]
607
+ "args": ["-y", "@yichus/mcp@0.1.2"]
379
608
  }
380
609
  }
381
610
  }