argsbarg 7.1.2 → 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 (58) hide show
  1. package/CHANGELOG.md +24 -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 +20 -0
  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/package.json +1 -1
  37. package/src/cli-tool/create.test.ts +14 -0
  38. package/src/cli-tool/create.ts +9 -0
  39. package/src/cli-tool/main.ts +1 -1
  40. package/src/cli-tool/schemagen/run.ts +41 -2
  41. package/src/cli-tool/schemagen/schemagen.test.ts +87 -2
  42. package/src/config/validate.ts +13 -31
  43. package/src/core/json-pointer.ts +46 -0
  44. package/src/core/validate.ts +64 -1
  45. package/src/headless/tool-call.test.ts +74 -2
  46. package/src/headless/tool-call.ts +44 -22
  47. package/src/http/schema-deref.ts +1 -23
  48. package/src/mcp/tools.test.ts +137 -3
  49. package/src/mcp/tools.ts +50 -5
  50. package/examples/mcp-plugin/.mcp.json +0 -6
  51. package/examples/mcp-plugin/mcp.json +0 -8
  52. package/examples/mcp-plugin/scripts/mcp.mjs +0 -11106
  53. package/examples/mcp-plugin/src/commands/render-json/__generated__/RenderJsonInputSchema.json +0 -15
  54. package/examples/mcp-plugin/src/commands/render-json/__generated__/index.ts +0 -5
  55. package/examples/mcp-plugin/src/commands/status/__generated__/StatusJsonOutputSchema.json +0 -15
  56. package/examples/mcp-plugin/src/commands/status/__generated__/index.ts +0 -5
  57. package/examples/mcp-plugin/src/commands/workspaces/__generated__/WorkspaceNameInputSchema.json +0 -15
  58. package/examples/mcp-plugin/src/commands/workspaces/__generated__/index.ts +0 -5
@@ -1,13 +1,21 @@
1
1
  /*
2
- Tests for mcp/tools module: MCP tool derivation, size reporting, and per-leaf MCP-only notes.
2
+ Tests for mcp/tools module: MCP tool derivation, size reporting, per-leaf MCP-only notes,
3
+ object-root schema wrapping, and the MCP tool schema startup check.
3
4
  */
4
5
 
5
6
  import { describe, expect, test } from "bun:test";
6
7
  import { cliPresentationRoot } from "../builtins/presentation.ts";
7
8
  import { CliOptionKind } from "../core/types.ts";
9
+ import { cliValidateProgram } from "../core/validate.ts";
8
10
  import { cliHelpRender } from "../help.ts";
9
- import { testProgram } from "../test/fixtures.ts";
10
- import { collectMcpTools, DEFAULT_MCP_SIZE_LIMITS, mcpSizeReport } from "./tools.ts";
11
+ import { requireMcpTool, testProgram } from "../test/fixtures.ts";
12
+ import {
13
+ collectMcpTools,
14
+ DEFAULT_MCP_SIZE_LIMITS,
15
+ MCP_INPUT_WRAPPER_KEY,
16
+ mcpSizeReport,
17
+ wrapMcpRootSchema,
18
+ } from "./tools.ts";
11
19
 
12
20
  describe("mcpSizeReport", () => {
13
21
  test("measures description and pretty definition against a hand-built JSON.stringify", () => {
@@ -156,3 +164,129 @@ describe("mcpTool.notes", () => {
156
164
  expect(help).toContain("Original CLI notes.");
157
165
  });
158
166
  });
167
+
168
+ /** Discriminated-union input: `anyOf` root with `$ref` branches, as ts-json-schema-generator writes it. */
169
+ const unionInputSchema: Record<string, unknown> = {
170
+ $schema: "http://json-schema.org/draft-07/schema#",
171
+ description: "Edit operation.",
172
+ anyOf: [{ $ref: "#/definitions/Append" }, { $ref: "#/definitions/Replace" }],
173
+ definitions: {
174
+ Append: {
175
+ type: "object",
176
+ properties: { kind: { const: "append" }, text: { type: "string" } },
177
+ required: ["kind", "text"],
178
+ additionalProperties: false,
179
+ },
180
+ Replace: {
181
+ type: "object",
182
+ properties: { kind: { const: "replace" }, find: { type: "string" }, text: { type: "string" } },
183
+ required: ["kind", "find", "text"],
184
+ additionalProperties: false,
185
+ },
186
+ },
187
+ };
188
+
189
+ describe("MCP object-root wrapping", () => {
190
+ test("object-rooted schemas pass through unchanged", () => {
191
+ const schema = { type: "object", properties: { a: { type: "string" } } };
192
+ expect(wrapMcpRootSchema(schema, MCP_INPUT_WRAPPER_KEY)).toEqual({ schema, wrapped: false });
193
+ });
194
+
195
+ test("union roots wrap under input with $schema and definitions moved to the new root", () => {
196
+ const { schema, wrapped } = wrapMcpRootSchema(unionInputSchema, MCP_INPUT_WRAPPER_KEY);
197
+ expect(wrapped).toBe(true);
198
+ expect(schema).toEqual({
199
+ $schema: unionInputSchema.$schema,
200
+ type: "object",
201
+ properties: { input: { description: "Edit operation.", anyOf: unionInputSchema.anyOf } },
202
+ required: ["input"],
203
+ additionalProperties: false,
204
+ definitions: unionInputSchema.definitions,
205
+ });
206
+ });
207
+
208
+ test("collectMcpTools wraps non-object input and output schemas and flags them", () => {
209
+ const program = testProgram({
210
+ key: "wrapapp",
211
+ description: "Wrap demo.",
212
+ mcpServer: { enabled: true },
213
+ commands: [
214
+ {
215
+ key: "edit",
216
+ description: "Edit.",
217
+ kind: "document",
218
+ inputSchema: unionInputSchema,
219
+ outputSchema: { type: "array", items: { type: "string" } },
220
+ handler: () => [],
221
+ },
222
+ { key: "plain", description: "Plain.", handler: () => {} },
223
+ ],
224
+ });
225
+ const tools = collectMcpTools(program);
226
+ const edit = requireMcpTool(tools, "edit");
227
+ expect(edit.inputWrapped).toBe(true);
228
+ expect(edit.inputSchema.type).toBe("object");
229
+ expect(edit.outputWrapped).toBe(true);
230
+ expect(edit.outputSchema).toEqual({
231
+ type: "object",
232
+ properties: { result: { type: "array", items: { type: "string" } } },
233
+ required: ["result"],
234
+ additionalProperties: false,
235
+ });
236
+ const plain = requireMcpTool(tools, "plain");
237
+ expect(plain.inputWrapped).toBe(false);
238
+ expect(plain.outputWrapped).toBe(false);
239
+ });
240
+ });
241
+
242
+ describe("MCP tool schema startup check", () => {
243
+ /** Program with one document leaf using `inputSchema`, MCP on or off. */
244
+ function schemaProgram(inputSchema: Record<string, unknown>, mcpEnabled: boolean) {
245
+ return testProgram({
246
+ key: "checkapp",
247
+ description: "Check demo.",
248
+ ...(mcpEnabled ? { mcpServer: { enabled: true } } : {}),
249
+ commands: [{ key: "run", description: "Run.", kind: "document", inputSchema, handler: () => {} }],
250
+ });
251
+ }
252
+
253
+ test("accepts wrapped schemas whose definitions still resolve", () => {
254
+ expect(() => cliValidateProgram(schemaProgram(unionInputSchema, true))).not.toThrow();
255
+ });
256
+
257
+ test("rejects an unresolved local $ref when MCP is enabled", () => {
258
+ const dangling = { $ref: "#/definitions/Missing", definitions: {} };
259
+ expect(() => cliValidateProgram(schemaProgram(dangling, true))).toThrow(
260
+ 'MCP tool "run" inputSchema has an unresolved $ref: #/definitions/Missing',
261
+ );
262
+ });
263
+
264
+ test('rejects $ref "#" in a wrapped schema', () => {
265
+ const recursive = { anyOf: [{ type: "object" }, { type: "array", items: { $ref: "#" } }] };
266
+ expect(() => cliValidateProgram(schemaProgram(recursive, true))).toThrow('uses $ref "#"');
267
+ });
268
+
269
+ test("skips the check when MCP is disabled", () => {
270
+ const dangling = { $ref: "#/definitions/Missing", definitions: {} };
271
+ expect(() => cliValidateProgram(schemaProgram(dangling, false))).not.toThrow();
272
+ });
273
+
274
+ test("skips MCP-hidden leaves", () => {
275
+ const program = testProgram({
276
+ key: "checkapp",
277
+ description: "Check demo.",
278
+ mcpServer: { enabled: true },
279
+ commands: [
280
+ {
281
+ key: "run",
282
+ description: "Run.",
283
+ kind: "document",
284
+ inputSchema: { $ref: "#/definitions/Missing", definitions: {} },
285
+ mcpTool: { hidden: true },
286
+ handler: () => {},
287
+ },
288
+ ],
289
+ });
290
+ expect(() => cliValidateProgram(program)).not.toThrow();
291
+ });
292
+ });
package/src/mcp/tools.ts CHANGED
@@ -49,10 +49,51 @@ export interface McpToolDef {
49
49
  path: string[];
50
50
  /** Leaf command node. */
51
51
  leaf: CliLeaf;
52
- /** JSON Schema for tools/call arguments. */
52
+ /** JSON Schema for tools/call arguments (wrapped under {@link MCP_INPUT_WRAPPER_KEY} when not object-rooted). */
53
53
  inputSchema: Record<string, unknown>;
54
- /** 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). */
55
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
+ };
56
97
  }
57
98
 
58
99
  /** Builds MCP tool description: "{cli path} — {description}". */
@@ -163,14 +204,18 @@ export function collectMcpTools(root: CliProgram): McpToolDef[] {
163
204
  if (isMcpHidden(cmd)) {
164
205
  return;
165
206
  }
166
- 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);
167
210
  out.push({
168
211
  name: mcpToolName(root, path),
169
212
  description: resolveToolDescription(root, path, cmd),
170
213
  path,
171
214
  leaf: cmd,
172
- inputSchema: buildLeafInputSchema(cmd),
173
- ...(outputSchema === undefined ? {} : { outputSchema }),
215
+ inputSchema: input.schema,
216
+ inputWrapped: input.wrapped,
217
+ ...(output === undefined ? {} : { outputSchema: output.schema }),
218
+ outputWrapped: output?.wrapped ?? false,
174
219
  });
175
220
  return;
176
221
  }
@@ -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
- }