argsbarg 7.1.1 → 7.1.3

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.
Files changed (71) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +7 -7
  3. package/docs/README.md +1 -1
  4. package/docs/cli-program.md +1 -1
  5. package/docs/configure.md +2 -2
  6. package/docs/mcp.md +53 -4
  7. package/docs/output-schema.md +6 -0
  8. package/examples/full-example/AGENTS.md +1 -1
  9. package/examples/full-example/bun.lock +83 -1
  10. package/examples/full-example/justfile +5 -3
  11. package/examples/full-example-json/AGENTS.md +1 -1
  12. package/examples/full-example-json/README.md +1 -0
  13. package/examples/full-example-json/bun.lock +83 -1
  14. package/examples/full-example-json/docs/cli-schema.json +284 -9
  15. package/examples/full-example-json/docs/cli.md +236 -18
  16. package/examples/full-example-json/docs/http.md +1 -0
  17. package/examples/full-example-json/docs/mcp.md +18 -0
  18. package/examples/full-example-json/docs/openapi.json +155 -0
  19. package/examples/full-example-json/justfile +5 -3
  20. package/examples/full-example-json/src/commands/shape-area/__generated__/ShapeAreaInputSchema.json +59 -0
  21. package/examples/full-example-json/src/commands/shape-area/__generated__/index.ts +5 -0
  22. package/examples/full-example-json/src/commands/shape-area/command.test.ts +34 -0
  23. package/examples/full-example-json/src/commands/shape-area/command.ts +31 -0
  24. package/examples/full-example-json/src/commands/shape-area/types.ts +28 -0
  25. package/examples/full-example-json/src/program.ts +2 -1
  26. package/examples/mcp-plugin/.claude-plugin/plugin.json +7 -1
  27. package/examples/mcp-plugin/.cursor-plugin/plugin.json +7 -1
  28. package/examples/mcp-plugin/AGENTS.md +14 -1
  29. package/examples/mcp-plugin/README.md +19 -10
  30. package/examples/mcp-plugin/bun.lock +83 -1
  31. package/examples/mcp-plugin/bunfig.toml +4 -0
  32. package/examples/mcp-plugin/docs/node-distro.md +97 -0
  33. package/examples/mcp-plugin/justfile +12 -11
  34. package/examples/mcp-plugin/package.json +2 -1
  35. package/examples/mcp-plugin/scripts/release.ts +10 -11
  36. package/index.d.ts +62 -0
  37. package/package.json +1 -1
  38. package/src/cli-tool/create.test.ts +14 -0
  39. package/src/cli-tool/create.ts +9 -0
  40. package/src/cli-tool/main.ts +1 -1
  41. package/src/cli-tool/schemagen/run.ts +41 -2
  42. package/src/cli-tool/schemagen/schemagen.test.ts +87 -2
  43. package/src/config/validate.test.ts +157 -0
  44. package/src/config/validate.ts +353 -26
  45. package/src/core/document-leaf.test.ts +53 -0
  46. package/src/core/json-pointer.ts +46 -0
  47. package/src/core/types.ts +33 -0
  48. package/src/core/validate.ts +68 -1
  49. package/src/docs/docs.test.ts +7 -0
  50. package/src/docs/mcp-guide.ts +43 -1
  51. package/src/headless/tool-call.test.ts +74 -2
  52. package/src/headless/tool-call.ts +44 -22
  53. package/src/http/schema-deref.ts +1 -23
  54. package/src/index.ts +3 -0
  55. package/src/mcp/server.ts +28 -4
  56. package/src/mcp/tools.test.ts +292 -0
  57. package/src/mcp/tools.ts +144 -6
  58. package/src/runtime/cli.ts +6 -1
  59. package/src/server/context.ts +6 -0
  60. package/src/test/integration/mcp.test.ts +73 -0
  61. package/src/test/mcp-integration-fixture.ts +1 -0
  62. package/src/test/mcp-size-fixture.ts +31 -0
  63. package/examples/mcp-plugin/.mcp.json +0 -6
  64. package/examples/mcp-plugin/mcp.json +0 -8
  65. package/examples/mcp-plugin/scripts/mcp.mjs +0 -11106
  66. package/examples/mcp-plugin/src/commands/render-json/__generated__/RenderJsonInputSchema.json +0 -15
  67. package/examples/mcp-plugin/src/commands/render-json/__generated__/index.ts +0 -5
  68. package/examples/mcp-plugin/src/commands/status/__generated__/StatusJsonOutputSchema.json +0 -15
  69. package/examples/mcp-plugin/src/commands/status/__generated__/index.ts +0 -5
  70. package/examples/mcp-plugin/src/commands/workspaces/__generated__/WorkspaceNameInputSchema.json +0 -15
  71. package/examples/mcp-plugin/src/commands/workspaces/__generated__/index.ts +0 -5
package/src/mcp/tools.ts CHANGED
@@ -6,6 +6,7 @@ flat JSON tool arguments into argv for Cli.invoke.
6
6
  import { cliSchemaJson } from "../core/schema.ts";
7
7
  import {
8
8
  type CliLeaf,
9
+ type CliMcpSizeLimits,
9
10
  type CliNode,
10
11
  type CliOption,
11
12
  CliOptionKind,
@@ -48,10 +49,51 @@ export interface McpToolDef {
48
49
  path: string[];
49
50
  /** Leaf command node. */
50
51
  leaf: CliLeaf;
51
- /** JSON Schema for tools/call arguments. */
52
+ /** JSON Schema for tools/call arguments (wrapped under {@link MCP_INPUT_WRAPPER_KEY} when not object-rooted). */
52
53
  inputSchema: Record<string, unknown>;
53
- /** JSON Schema for structured tool results when set on the leaf `mcpTool`. */
54
+ /** True when {@link inputSchema} wraps the leaf schema; tools/call arguments are unwrapped before invoke. */
55
+ inputWrapped: boolean;
56
+ /** JSON Schema for structured tool results (wrapped under {@link MCP_OUTPUT_WRAPPER_KEY} when not object-rooted). */
54
57
  outputSchema?: Record<string, unknown>;
58
+ /** True when {@link outputSchema} wraps the leaf schema; `structuredContent` is wrapped to match. */
59
+ outputWrapped: boolean;
60
+ }
61
+
62
+ /** Property holding the leaf input when an MCP `inputSchema` is wrapped to get an object root. */
63
+ export const MCP_INPUT_WRAPPER_KEY = "input";
64
+
65
+ /** Property holding the leaf result when an MCP `outputSchema` is wrapped to get an object root. */
66
+ export const MCP_OUTPUT_WRAPPER_KEY = "result";
67
+
68
+ /**
69
+ * MCP requires `type: "object"` at the root of tool input and output schemas. Returns object-rooted
70
+ * schemas unchanged; wraps anything else (e.g. a discriminated-union `anyOf` root) as a single
71
+ * required property, moving `$schema`, `$id`, `definitions`, and `$defs` up so `#/definitions/…`
72
+ * references still resolve.
73
+ */
74
+ export function wrapMcpRootSchema(
75
+ /** Leaf input or output schema. */
76
+ schema: Record<string, unknown>,
77
+ /** Wrapper property name ({@link MCP_INPUT_WRAPPER_KEY} or {@link MCP_OUTPUT_WRAPPER_KEY}). */
78
+ key: string,
79
+ ): { schema: Record<string, unknown>; wrapped: boolean } {
80
+ if (schema.type === "object") {
81
+ return { schema, wrapped: false };
82
+ }
83
+ const { $schema, $id, definitions, $defs, ...inner } = schema;
84
+ return {
85
+ schema: {
86
+ ...($schema === undefined ? {} : { $schema }),
87
+ ...($id === undefined ? {} : { $id }),
88
+ type: "object",
89
+ properties: { [key]: inner },
90
+ required: [key],
91
+ additionalProperties: false,
92
+ ...(definitions === undefined ? {} : { definitions }),
93
+ ...($defs === undefined ? {} : { $defs }),
94
+ },
95
+ wrapped: true,
96
+ };
55
97
  }
56
98
 
57
99
  /** Builds MCP tool description: "{cli path} — {description}". */
@@ -107,7 +149,13 @@ function resolveToolDescription(root: CliProgram, path: string[], leaf: CliLeaf)
107
149
  } else {
108
150
  desc = mcpToolDescription(path, root.key, leaf.description);
109
151
  }
110
- const notes = (leaf.notes ?? "").trim();
152
+ // `mcpTool.notes` overrides what CLI help shows (leaf.notes) for the MCP description only:
153
+ // `false` omits notes entirely; a string replaces them; omitted falls through to leaf.notes.
154
+ const notesOverride = leaf.mcpTool?.notes;
155
+ if (notesOverride === false) {
156
+ return desc;
157
+ }
158
+ const notes = (typeof notesOverride === "string" ? notesOverride : (leaf.notes ?? "")).trim();
111
159
  if (notes.length > 0) {
112
160
  desc += `\n\n${cliResolveNotes(notes, root.key)}`;
113
161
  }
@@ -156,14 +204,18 @@ export function collectMcpTools(root: CliProgram): McpToolDef[] {
156
204
  if (isMcpHidden(cmd)) {
157
205
  return;
158
206
  }
159
- const outputSchema = leafOutputSchema(cmd);
207
+ const input = wrapMcpRootSchema(buildLeafInputSchema(cmd), MCP_INPUT_WRAPPER_KEY);
208
+ const leafOutput = leafOutputSchema(cmd);
209
+ const output = leafOutput === undefined ? undefined : wrapMcpRootSchema(leafOutput, MCP_OUTPUT_WRAPPER_KEY);
160
210
  out.push({
161
211
  name: mcpToolName(root, path),
162
212
  description: resolveToolDescription(root, path, cmd),
163
213
  path,
164
214
  leaf: cmd,
165
- inputSchema: buildLeafInputSchema(cmd),
166
- ...(outputSchema === undefined ? {} : { outputSchema }),
215
+ inputSchema: input.schema,
216
+ inputWrapped: input.wrapped,
217
+ ...(output === undefined ? {} : { outputSchema: output.schema }),
218
+ outputWrapped: output?.wrapped ?? false,
167
219
  });
168
220
  return;
169
221
  }
@@ -272,3 +324,89 @@ export function mcpToolCallToArgv(
272
324
 
273
325
  return argv;
274
326
  }
327
+
328
+ /** Default {@link CliMcpSizeLimits}; see that type for what each limit approximates and why. */
329
+ export const DEFAULT_MCP_SIZE_LIMITS: Required<CliMcpSizeLimits> = {
330
+ definitionBytes: 51_200,
331
+ definitionLines: 2_000,
332
+ descriptionChars: 2_048,
333
+ instructionsChars: 2_048,
334
+ };
335
+
336
+ /** Measured size of one MCP tool's description and pretty-printed definition. */
337
+ export interface McpToolSize {
338
+ /** Pretty-printed `{name, description, inputSchema, outputSchema}`, in UTF-8 bytes. */
339
+ definitionBytes: number;
340
+ /** Line count of the same pretty-printed definition. */
341
+ definitionLines: number;
342
+ /** Character length of `description` alone. */
343
+ descriptionChars: number;
344
+ /** MCP tool name. */
345
+ name: string;
346
+ }
347
+
348
+ /** Per-tool sizes plus any warnings past {@link CliMcpSizeLimits} (defaults or `mcpServer.sizeLimits`). */
349
+ export interface McpSizeReport {
350
+ /** Character length of `mcpServer.instructions`, or 0 when unset. */
351
+ instructionsChars: number;
352
+ /** One entry per MCP tool, in `tools/list` order. */
353
+ tools: McpToolSize[];
354
+ /** Human-readable warnings for anything past its limit; empty when everything fits. */
355
+ warnings: string[];
356
+ }
357
+
358
+ /** Formats a definition's pretty-printed JSON exactly as Cursor's synced tool file would show it. */
359
+ function mcpToolDefinitionJson(tool: McpToolDef): string {
360
+ return JSON.stringify(
361
+ {
362
+ name: tool.name,
363
+ description: tool.description,
364
+ inputSchema: tool.inputSchema,
365
+ ...(tool.outputSchema === undefined ? {} : { outputSchema: tool.outputSchema }),
366
+ },
367
+ null,
368
+ 2,
369
+ );
370
+ }
371
+
372
+ /** Measures every MCP tool's description and definition size against {@link CliMcpSizeLimits}. */
373
+ export function mcpSizeReport(root: CliProgram): McpSizeReport {
374
+ const limits = { ...DEFAULT_MCP_SIZE_LIMITS, ...root.mcpServer?.sizeLimits };
375
+ const warnings: string[] = [];
376
+
377
+ const tools = collectMcpTools(root).map((tool): McpToolSize => {
378
+ const definitionJson = mcpToolDefinitionJson(tool);
379
+ const definitionBytes = Buffer.byteLength(definitionJson, "utf8");
380
+ const definitionLines = definitionJson.split("\n").length;
381
+ const descriptionChars = tool.description.length;
382
+
383
+ if (limits.descriptionChars !== false && descriptionChars > limits.descriptionChars) {
384
+ warnings.push(
385
+ `MCP tool "${tool.name}" description is ${descriptionChars.toLocaleString()} chars ` +
386
+ `(limit ${limits.descriptionChars.toLocaleString()}; Claude Code truncates longer descriptions)`,
387
+ );
388
+ }
389
+ const overBytes = limits.definitionBytes !== false && definitionBytes > limits.definitionBytes;
390
+ const overLines = limits.definitionLines !== false && definitionLines > limits.definitionLines;
391
+ if (overBytes || overLines) {
392
+ const byteLimit = limits.definitionBytes === false ? "∞" : limits.definitionBytes.toLocaleString();
393
+ const lineLimit = limits.definitionLines === false ? "∞" : limits.definitionLines.toLocaleString();
394
+ warnings.push(
395
+ `MCP tool "${tool.name}" definition is ${definitionBytes.toLocaleString()} bytes / ` +
396
+ `${definitionLines.toLocaleString()} lines pretty-printed (limit ${byteLimit} bytes / ${lineLimit} lines; ` +
397
+ `Cursor reads tool definitions in chunks of at most that size)`,
398
+ );
399
+ }
400
+
401
+ return { definitionBytes, definitionLines, descriptionChars, name: tool.name };
402
+ });
403
+
404
+ const instructionsChars = (root.mcpServer?.instructions ?? "").length;
405
+ if (limits.instructionsChars !== false && instructionsChars > limits.instructionsChars) {
406
+ warnings.push(
407
+ `MCP instructions are ${instructionsChars.toLocaleString()} chars (limit ${limits.instructionsChars.toLocaleString()})`,
408
+ );
409
+ }
410
+
411
+ return { instructionsChars, tools, warnings };
412
+ }
@@ -33,7 +33,8 @@ import { buildInvokeHookContext, classifyFailureKind, runErrorPipeline, runHook
33
33
  import { httpServeHttp } from "../http/server.ts";
34
34
  import { LogEmitter } from "../log/emitter.ts";
35
35
  import { bootstrapMcpEnv } from "../mcp/env.ts";
36
- import { mcpServeStdioLoop } from "../mcp/server.ts";
36
+ import { MCP_PROTOCOL_VERSIONS, mcpServeStdioLoop } from "../mcp/server.ts";
37
+ import { mcpSizeReport } from "../mcp/tools.ts";
37
38
  import { createServerRuntime, type ServerHandleContext } from "../server/context.ts";
38
39
  import { resolveHttpServeConfig, resolveMcpServeConfig, type ServeOverrides } from "../server/overrides.ts";
39
40
  import {
@@ -414,6 +415,7 @@ export class Cli {
414
415
  emitter,
415
416
  mcp: resolved,
416
417
  mcpHooks: this.program.mcpServer?.hooks,
418
+ mcpProtocolVersion: MCP_PROTOCOL_VERSIONS[0],
417
419
  };
418
420
  bootstrapAppConfig(this.program, { validateFile: "soft", runtime, emitter });
419
421
  const shutdown = () => {
@@ -422,6 +424,9 @@ export class Cli {
422
424
  };
423
425
  process.once("SIGINT", shutdown);
424
426
  process.once("SIGTERM", shutdown);
427
+ for (const message of mcpSizeReport(this.program).warnings) {
428
+ emitter.emit({ level: "warn", message, action: "mcp.size" });
429
+ }
425
430
  emitter.emitLifecycle(`${this.program.key} ${this.program.version} — MCP ready (stdio)`, "mcp.server.ready");
426
431
  await mcpServeStdioLoop(this);
427
432
  process.exit(0);
@@ -14,6 +14,12 @@ export interface ServerHandleContext {
14
14
  mcp?: ResolvedMcpServeConfig;
15
15
  httpHooks?: CliHttpWireHooks;
16
16
  mcpHooks?: CliMcpWireHooks;
17
+ /**
18
+ * The MCP protocol version negotiated with `initialize`, or the newest supported version before
19
+ * `initialize` has been handled. Later requests (`tools/list`, `tools/call`) gate version-specific
20
+ * response fields (e.g. `outputSchema`, `structuredContent`) on this.
21
+ */
22
+ mcpProtocolVersion?: string;
17
23
  }
18
24
 
19
25
  /** Creates a fresh {@link ServerRuntime} for HTTP or MCP. */
@@ -353,6 +353,79 @@ test("MCP initialize returns tools and resources capabilities", async () => {
353
353
  expect(res.result.capabilities.resources).toBeDefined();
354
354
  });
355
355
 
356
+ test("MCP initialize echoes a supported protocol version", async () => {
357
+ for (const version of ["2024-11-05", "2025-06-18"]) {
358
+ const responses = await mcpRequest([
359
+ { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: version } },
360
+ ]);
361
+ const res = responses.get(1) as { result: { protocolVersion: string } };
362
+ expect(res.result.protocolVersion).toBe(version);
363
+ }
364
+ });
365
+
366
+ test("MCP initialize answers unsupported or missing versions with the newest", async () => {
367
+ for (const params of [{ protocolVersion: "2099-01-01" }, {}]) {
368
+ const responses = await mcpRequest([{ jsonrpc: "2.0", id: 1, method: "initialize", params }]);
369
+ const res = responses.get(1) as { result: { protocolVersion: string } };
370
+ expect(res.result.protocolVersion).toBe("2025-06-18");
371
+ }
372
+ });
373
+
374
+ test("2024-11-05 sessions omit outputSchema and structuredContent", async () => {
375
+ const readme = join(import.meta.dir, "..", "..", "..", "README.md");
376
+ const responses = await mcpRequest([
377
+ { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05" } },
378
+ { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} },
379
+ {
380
+ jsonrpc: "2.0",
381
+ id: 3,
382
+ method: "tools/call",
383
+ params: { name: "stat_owner_lookup", arguments: { path: readme, "user-name": "test" } },
384
+ },
385
+ ]);
386
+ const listRes = responses.get(2) as { result: { tools: { name: string; outputSchema?: unknown }[] } };
387
+ const lookup = listRes.result.tools.find((t) => t.name === "stat_owner_lookup");
388
+ expect(lookup).toBeDefined();
389
+ expect(lookup?.outputSchema).toBeUndefined();
390
+
391
+ const callRes = responses.get(3) as { result: { structuredContent?: unknown; isError: boolean } };
392
+ expect(callRes.result.isError).toBe(false);
393
+ expect(callRes.result.structuredContent).toBeUndefined();
394
+ });
395
+
396
+ test("MCP initialize includes configured instructions", async () => {
397
+ const responses = await mcpRequest([{ jsonrpc: "2.0", id: 1, method: "initialize", params: {} }], {
398
+ script: "src/test/mcp-integration-fixture.ts",
399
+ });
400
+ const res = responses.get(1) as { result: { instructions?: string } };
401
+ expect(res.result.instructions).toBe("Read the fixture skill.");
402
+ });
403
+
404
+ test("MCP startup warns about oversized tools on stderr", async () => {
405
+ const proc = Bun.spawn(["bun", "run", "src/test/mcp-size-fixture.ts", "mcp"], {
406
+ stdin: "pipe",
407
+ stdout: "pipe",
408
+ stderr: "pipe",
409
+ });
410
+ proc.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: {} })}\n`);
411
+ proc.stdin.end();
412
+ const timeout = setTimeout(() => proc.kill(), 10_000);
413
+ const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
414
+ await proc.exited;
415
+ clearTimeout(timeout);
416
+
417
+ expect(stderr).toContain("description is 3,");
418
+
419
+ const lines = stdout
420
+ .split("\n")
421
+ .map((l) => l.trim())
422
+ .filter(Boolean);
423
+ expect(lines.length).toBeGreaterThan(0);
424
+ for (const line of lines) {
425
+ expect(() => JSON.parse(line)).not.toThrow();
426
+ }
427
+ });
428
+
356
429
  test("MCP tools/list includes stat_owner_lookup", async () => {
357
430
  const responses = await mcpRequest([{ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }]);
358
431
  const res = responses.get(2) as {
@@ -68,6 +68,7 @@ const program = {
68
68
  key: "mcp-test",
69
69
  mcpServer: {
70
70
  enabled: true,
71
+ instructions: "Read the fixture skill.",
71
72
  resources: [
72
73
  {
73
74
  uri: "test://hello",
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env bun
2
+ /*
3
+ MCP size-warning integration test fixture (not a public example). One leaf with oversized notes,
4
+ triggering the "description" startup size warning on stderr.
5
+ */
6
+
7
+ import { Cli, type CliProgram } from "../index.ts";
8
+
9
+ const program = {
10
+ commands: [
11
+ {
12
+ key: "run",
13
+ description: "Run it.",
14
+ notes: "x".repeat(3_000),
15
+ handler: (ctx) => {
16
+ if (ctx.invocation === "cli") {
17
+ console.log("ran");
18
+ return;
19
+ }
20
+ return "ran";
21
+ },
22
+ },
23
+ ],
24
+ description: "MCP size-warning test fixture.",
25
+ key: "mcp-size-test",
26
+ mcpServer: { enabled: true },
27
+ version: "0.0.0-test",
28
+ } satisfies CliProgram;
29
+
30
+ const cli = new Cli(program);
31
+ await cli.run();
@@ -1,6 +0,0 @@
1
- {
2
- "mcp-plugin": {
3
- "command": "node",
4
- "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/mcp.mjs", "mcp"]
5
- }
6
- }
@@ -1,8 +0,0 @@
1
- {
2
- "mcpServers": {
3
- "mcp-plugin": {
4
- "command": "node",
5
- "args": ["${CURSOR_PLUGIN_ROOT}/scripts/mcp.mjs", "mcp"]
6
- }
7
- }
8
- }