@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/README.md +80 -20
- package/dist/cli.js +323 -114
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +18 -7
- package/dist/index.js +290 -64
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/LICENSE +0 -21
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 {
|
|
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
|
-
|
|
172
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
185
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
|
203
|
-
registerStatic(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
name
|
|
292
|
-
description
|
|
293
|
-
inputSchema:
|
|
294
|
-
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
|
354
|
-
|
|
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
|
-
|
|
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": "
|
|
378
|
-
"args": ["
|
|
586
|
+
"command": "node",
|
|
587
|
+
"args": ["/absolute/path/to/yichus/packages/yichus-mcp/dist/cli.js"]
|
|
379
588
|
}
|
|
380
589
|
}
|
|
381
590
|
}
|