toolcraft 0.0.39 → 0.0.41

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 (70) hide show
  1. package/README.md +41 -4
  2. package/dist/cli.js +40 -9
  3. package/dist/http-errors.d.ts +57 -0
  4. package/dist/http-errors.js +72 -0
  5. package/dist/index.d.ts +10 -0
  6. package/dist/index.js +15 -0
  7. package/dist/mcp-proxy.js +16 -1
  8. package/dist/mcp.js +224 -12
  9. package/dist/number-schema.js +9 -2
  10. package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.js +1 -1
  11. package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.js +6 -1
  12. package/node_modules/@poe-code/agent-defs/dist/agents/cursor.js +1 -1
  13. package/node_modules/@poe-code/agent-defs/dist/agents/kimi.js +1 -1
  14. package/node_modules/@poe-code/agent-defs/dist/agents/opencode.js +1 -1
  15. package/node_modules/@poe-code/agent-defs/dist/specifier.js +15 -6
  16. package/node_modules/@poe-code/agent-defs/dist/types.d.ts +5 -0
  17. package/node_modules/@poe-code/agent-mcp-config/dist/apply.js +36 -3
  18. package/node_modules/@poe-code/frontmatter/README.md +35 -0
  19. package/node_modules/@poe-code/frontmatter/dist/fences.d.ts +8 -0
  20. package/node_modules/@poe-code/frontmatter/dist/fences.js +72 -0
  21. package/node_modules/@poe-code/frontmatter/dist/index.d.ts +3 -0
  22. package/node_modules/@poe-code/frontmatter/dist/index.js +3 -0
  23. package/node_modules/@poe-code/frontmatter/dist/parse.d.ts +20 -0
  24. package/node_modules/@poe-code/frontmatter/dist/parse.js +137 -0
  25. package/node_modules/@poe-code/frontmatter/dist/stringify.d.ts +1 -0
  26. package/node_modules/@poe-code/frontmatter/dist/stringify.js +35 -0
  27. package/node_modules/@poe-code/frontmatter/package.json +25 -0
  28. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.d.ts +1 -0
  29. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +25 -1
  30. package/node_modules/tiny-mcp-client/README.md +7 -0
  31. package/node_modules/tiny-mcp-client/dist/internal.d.ts +2 -0
  32. package/node_modules/tiny-mcp-client/dist/internal.js +3 -0
  33. package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.js +10 -0
  34. package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +2 -8
  35. package/node_modules/tiny-mcp-client/src/internal.ts +6 -0
  36. package/node_modules/tiny-mcp-client/src/mcp-tool-types.compile-check.ts +10 -0
  37. package/node_modules/toolcraft-design/README.md +12 -0
  38. package/node_modules/toolcraft-design/dist/prompts/index.d.ts +39 -14
  39. package/node_modules/toolcraft-design/dist/prompts/index.js +10 -6
  40. package/node_modules/toolcraft-design/dist/prompts/interactive/cancel-symbol.d.ts +2 -0
  41. package/node_modules/toolcraft-design/dist/prompts/interactive/cancel-symbol.js +4 -0
  42. package/node_modules/toolcraft-design/dist/prompts/interactive/confirm.d.ts +9 -0
  43. package/node_modules/toolcraft-design/dist/prompts/interactive/confirm.js +47 -0
  44. package/node_modules/toolcraft-design/dist/prompts/interactive/core.d.ts +55 -0
  45. package/node_modules/toolcraft-design/dist/prompts/interactive/core.js +274 -0
  46. package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.d.ts +20 -0
  47. package/node_modules/toolcraft-design/dist/prompts/interactive/glyphs.js +53 -0
  48. package/node_modules/toolcraft-design/dist/prompts/interactive/index.d.ts +6 -0
  49. package/node_modules/toolcraft-design/dist/prompts/interactive/index.js +6 -0
  50. package/node_modules/toolcraft-design/dist/prompts/interactive/keys.d.ts +2 -0
  51. package/node_modules/toolcraft-design/dist/prompts/interactive/keys.js +28 -0
  52. package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.d.ts +13 -0
  53. package/node_modules/toolcraft-design/dist/prompts/interactive/multiselect.js +131 -0
  54. package/node_modules/toolcraft-design/dist/prompts/interactive/pagination.d.ts +10 -0
  55. package/node_modules/toolcraft-design/dist/prompts/interactive/pagination.js +52 -0
  56. package/node_modules/toolcraft-design/dist/prompts/interactive/password.d.ts +10 -0
  57. package/node_modules/toolcraft-design/dist/prompts/interactive/password.js +55 -0
  58. package/node_modules/toolcraft-design/dist/prompts/interactive/select.d.ts +18 -0
  59. package/node_modules/toolcraft-design/dist/prompts/interactive/select.js +89 -0
  60. package/node_modules/toolcraft-design/dist/prompts/interactive/test-helpers.d.ts +21 -0
  61. package/node_modules/toolcraft-design/dist/prompts/interactive/test-helpers.js +32 -0
  62. package/node_modules/toolcraft-design/dist/prompts/interactive/text.d.ts +12 -0
  63. package/node_modules/toolcraft-design/dist/prompts/interactive/text.js +60 -0
  64. package/node_modules/toolcraft-design/dist/prompts/interactive/wrap.d.ts +4 -0
  65. package/node_modules/toolcraft-design/dist/prompts/interactive/wrap.js +18 -0
  66. package/node_modules/toolcraft-design/dist/prompts/primitives/cancel.d.ts +1 -1
  67. package/node_modules/toolcraft-design/dist/prompts/primitives/cancel.js +1 -1
  68. package/node_modules/toolcraft-design/dist/terminal-markdown/parser/frontmatter.js +20 -453
  69. package/node_modules/toolcraft-design/package.json +6 -3
  70. package/package.json +9 -6
package/dist/mcp.js CHANGED
@@ -128,14 +128,24 @@ function formatAvailableList(values) {
128
128
  return `Available: ${[...values].sort().join(", ")}.`;
129
129
  }
130
130
  function applySchemaCasing(schema, casing) {
131
+ const oneOf = schema.oneOf?.map((child) => applySchemaCasing(child, casing));
132
+ const additionalProperties = typeof schema.additionalProperties === "object" && schema.additionalProperties !== null
133
+ ? applySchemaCasing(schema.additionalProperties, casing)
134
+ : schema.additionalProperties;
131
135
  if (schema.type !== "object" || schema.properties === undefined) {
132
136
  if (schema.type === "array" && schema.items !== undefined) {
133
137
  return {
134
138
  ...schema,
135
- items: applySchemaCasing(schema.items, casing)
139
+ ...(additionalProperties === undefined ? {} : { additionalProperties }),
140
+ items: applySchemaCasing(schema.items, casing),
141
+ ...(oneOf === undefined ? {} : { oneOf })
136
142
  };
137
143
  }
138
- return schema;
144
+ return {
145
+ ...schema,
146
+ ...(additionalProperties === undefined ? {} : { additionalProperties }),
147
+ ...(oneOf === undefined ? {} : { oneOf })
148
+ };
139
149
  }
140
150
  const properties = Object.fromEntries(Object.entries(schema.properties).map(([key, value]) => [
141
151
  formatSegment(key, casing),
@@ -144,8 +154,10 @@ function applySchemaCasing(schema, casing) {
144
154
  const required = schema.required?.map((key) => formatSegment(key, casing));
145
155
  return {
146
156
  ...schema,
157
+ ...(additionalProperties === undefined ? {} : { additionalProperties }),
147
158
  properties,
148
- ...(required === undefined ? {} : { required })
159
+ ...(required === undefined ? {} : { required }),
160
+ ...(oneOf === undefined ? {} : { oneOf })
149
161
  };
150
162
  }
151
163
  function collectParamSummaries(schema, casing, path = [], inheritedOptional = false) {
@@ -162,16 +174,32 @@ function collectParamSummaries(schema, casing, path = [], inheritedOptional = fa
162
174
  }
163
175
  return summaries;
164
176
  }
165
- function buildToolDescription(description, params, casing) {
177
+ function buildToolDescription(description, params, examples, commandName, casing) {
166
178
  const summary = collectParamSummaries(params, casing);
167
179
  const parameterSummary = summary.length === 0 ? "" : `Parameters: ${summary.join(", ")}.`;
180
+ const exampleSummary = examples.length === 0
181
+ ? ""
182
+ : `\n\nExamples:\n${examples
183
+ .map((example) => `- ${example.title}: ${commandName} ${formatMcpExampleParams(example.params)}`)
184
+ .join("\n")}`;
168
185
  if (description === undefined) {
169
- return parameterSummary;
186
+ return `${parameterSummary}${exampleSummary}`;
170
187
  }
171
188
  if (parameterSummary.length === 0) {
172
- return description;
189
+ return `${description}${exampleSummary}`;
173
190
  }
174
- return `${description} ${parameterSummary}`;
191
+ return `${description} ${parameterSummary}${exampleSummary}`;
192
+ }
193
+ function formatMcpExampleParams(params) {
194
+ return Object.entries(params)
195
+ .map(([key, value]) => `${key}=${formatMcpExampleValue(value)}`)
196
+ .join(" ");
197
+ }
198
+ function formatMcpExampleValue(value) {
199
+ if (typeof value === "string") {
200
+ return value;
201
+ }
202
+ return JSON.stringify(value);
175
203
  }
176
204
  function matchesAllowlist(toolName, allowlist) {
177
205
  if (allowlist === undefined) {
@@ -216,6 +244,9 @@ function enumerateTools(root, casing, allowlist, omitRootToolNamePrefix) {
216
244
  throw new ToolcraftBugError(`command "${name}" must define an object params schema for MCP.`);
217
245
  }
218
246
  validateUniqueMCPParameterFields(params, casing);
247
+ if (node.result !== undefined) {
248
+ validateUniqueMCPParameterFields(node.result, casing);
249
+ }
219
250
  const resolvedCommandPath = [...commandPath, node.name].join(".");
220
251
  const existingPath = commandPathsByToolName.get(name);
221
252
  if (existingPath !== undefined) {
@@ -226,8 +257,14 @@ function enumerateTools(root, casing, allowlist, omitRootToolNamePrefix) {
226
257
  command: node,
227
258
  commandPath: resolvedCommandPath,
228
259
  name,
229
- description: buildToolDescription(node.description, params, casing),
230
- inputSchema: applySchemaCasing(toJsonSchema(params), casing)
260
+ description: buildToolDescription(node.description, params, node.examples, node.name, casing),
261
+ inputSchema: applySchemaCasing(toJsonSchema(params), casing),
262
+ ...(node.result === undefined
263
+ ? {}
264
+ : {
265
+ outputSchema: applySchemaCasing(toJsonSchema(node.result), casing),
266
+ resultSchema: node.result
267
+ })
231
268
  });
232
269
  return;
233
270
  }
@@ -466,6 +503,166 @@ function validateToolArguments(schema, argumentsValue, casing) {
466
503
  throwValidationErrors(errors);
467
504
  return result;
468
505
  }
506
+ function serializeResultValue(schema, value, casing, label, errors) {
507
+ const unwrappedSchema = unwrapOptional(schema);
508
+ if (value === null && unwrappedSchema.nullable === true) {
509
+ return null;
510
+ }
511
+ switch (unwrappedSchema.kind) {
512
+ case "object":
513
+ return serializeResultObject(unwrappedSchema, value, casing, label, errors);
514
+ case "array":
515
+ if (!Array.isArray(value)) {
516
+ errors.push({
517
+ path: label,
518
+ message: `Invalid value for "${label}". Expected an array, got ${describeReceived(value)}.`
519
+ });
520
+ return value;
521
+ }
522
+ return value.map((item, index) => serializeResultValue(unwrappedSchema.item, item, casing, `${label}[${index}]`, errors));
523
+ case "record":
524
+ if (!isPlainObject(value)) {
525
+ errors.push({
526
+ path: label,
527
+ message: `Invalid value for "${label}". Expected an object, got ${describeReceived(value)}.`
528
+ });
529
+ return value;
530
+ }
531
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
532
+ key,
533
+ serializeResultValue(unwrappedSchema.value, item, casing, `${label}.${key}`, errors)
534
+ ]));
535
+ case "oneOf": {
536
+ if (!isPlainObject(value)) {
537
+ errors.push({
538
+ path: label,
539
+ message: `Invalid value for "${label}". Expected an object, got ${describeReceived(value)}.`
540
+ });
541
+ return value;
542
+ }
543
+ const discriminator = value[unwrappedSchema.discriminator];
544
+ const branch = typeof discriminator === "string" ? unwrappedSchema.branches[discriminator] : undefined;
545
+ if (branch === undefined) {
546
+ const branchNames = Object.keys(unwrappedSchema.branches);
547
+ errors.push({
548
+ path: label.length === 0 ? unwrappedSchema.discriminator : `${label}.${unwrappedSchema.discriminator}`,
549
+ message: `Invalid value for "${label.length === 0 ? unwrappedSchema.discriminator : `${label}.${unwrappedSchema.discriminator}`}". Expected one of: ${branchNames.join(", ")}, got ${describeReceived(discriminator)}.`
550
+ });
551
+ return value;
552
+ }
553
+ const { [unwrappedSchema.discriminator]: ignoredDiscriminator, ...branchValue } = value;
554
+ void ignoredDiscriminator;
555
+ return {
556
+ [formatSegment(unwrappedSchema.discriminator, casing)]: discriminator,
557
+ ...serializeResultObject(branch, branchValue, casing, label, errors)
558
+ };
559
+ }
560
+ case "union": {
561
+ if (!isPlainObject(value)) {
562
+ errors.push({
563
+ path: label,
564
+ message: `Invalid value for "${label}". Expected an object, got ${describeReceived(value)}.`
565
+ });
566
+ return value;
567
+ }
568
+ const branch = unwrappedSchema.branches.find((candidate) => Object.keys(candidate.shape).every((key) => candidate.shape[key]?.kind === "optional" ||
569
+ Object.prototype.hasOwnProperty.call(value, key)));
570
+ if (branch === undefined) {
571
+ errors.push({
572
+ path: label,
573
+ message: `Invalid value for "${label}". Expected one union branch, got ${describeReceived(value)}.`
574
+ });
575
+ return value;
576
+ }
577
+ return serializeResultObject(branch, value, casing, label, errors);
578
+ }
579
+ default:
580
+ return validateSchemaValue(schema, value, casing, label, errors);
581
+ }
582
+ }
583
+ function serializeResultObject(schema, value, casing, label, errors) {
584
+ if (!isPlainObject(value)) {
585
+ errors.push({
586
+ path: label,
587
+ message: `Invalid value for "${label}". Expected an object, got ${describeReceived(value)}.`
588
+ });
589
+ return {};
590
+ }
591
+ const result = {};
592
+ const expectedKeys = new Set(Object.keys(schema.shape));
593
+ for (const key of Object.keys(value)) {
594
+ if (!expectedKeys.has(key) && schema.additionalProperties !== true) {
595
+ const fieldLabel = label.length === 0 ? key : `${label}.${key}`;
596
+ errors.push({
597
+ path: fieldLabel,
598
+ message: `Unexpected result field "${fieldLabel}". ${formatAvailableList([...expectedKeys].map((expectedKey) => label.length === 0 ? expectedKey : `${label}.${expectedKey}`))}`
599
+ });
600
+ }
601
+ }
602
+ for (const [sourceKey, rawChildSchema] of Object.entries(schema.shape)) {
603
+ const childSchema = unwrapOptional(rawChildSchema);
604
+ const hasValue = Object.prototype.hasOwnProperty.call(value, sourceKey);
605
+ const wireKey = formatSegment(sourceKey, casing);
606
+ const fieldLabel = label.length === 0 ? sourceKey : `${label}.${sourceKey}`;
607
+ if (!hasValue) {
608
+ if (childSchema.default !== undefined) {
609
+ Object.defineProperty(result, wireKey, {
610
+ value: childSchema.default,
611
+ enumerable: true,
612
+ configurable: true,
613
+ writable: true
614
+ });
615
+ continue;
616
+ }
617
+ if (isOptional(rawChildSchema)) {
618
+ continue;
619
+ }
620
+ errors.push({ path: fieldLabel, message: `Missing required result field "${fieldLabel}".` });
621
+ continue;
622
+ }
623
+ Object.defineProperty(result, wireKey, {
624
+ value: serializeResultValue(rawChildSchema, value[sourceKey], casing, fieldLabel, errors),
625
+ enumerable: true,
626
+ configurable: true,
627
+ writable: true
628
+ });
629
+ }
630
+ if (schema.additionalProperties === true) {
631
+ for (const key of Object.keys(value)) {
632
+ if (!expectedKeys.has(key)) {
633
+ Object.defineProperty(result, key, {
634
+ value: value[key],
635
+ enumerable: true,
636
+ configurable: true,
637
+ writable: true
638
+ });
639
+ }
640
+ }
641
+ }
642
+ return result;
643
+ }
644
+ function validateCommandResult(schema, value, casing) {
645
+ const errors = [];
646
+ const result = serializeResultObject(schema, value, casing, "", errors);
647
+ throwResultValidationErrors(errors);
648
+ return result;
649
+ }
650
+ function throwResultValidationErrors(errors) {
651
+ if (errors.length === 0) {
652
+ return;
653
+ }
654
+ if (errors.length === 1) {
655
+ throw new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, errors[0]?.message ?? "Invalid command result.");
656
+ }
657
+ const rendered = errors
658
+ .slice(0, 10)
659
+ .map((error) => ` - ${error.path}: ${error.message}`);
660
+ const remaining = errors.length - rendered.length;
661
+ if (remaining > 0) {
662
+ rendered.push(` ... and ${remaining} more`);
663
+ }
664
+ throw new ToolError(JSON_RPC_ERROR_CODES.INTERNAL_ERROR, `${errors.length} result errors:\n${rendered.join("\n")}`);
665
+ }
469
666
  function isContentBlock(value) {
470
667
  if (!isPlainObject(value) || typeof value.type !== "string") {
471
668
  return false;
@@ -491,7 +688,9 @@ function toToolContent(result) {
491
688
  if (isContentBlock(result)) {
492
689
  return [result];
493
690
  }
494
- return [{ type: "text", text: JSON.stringify(result) }];
691
+ const fallbackValue = result;
692
+ const fallbackText = JSON.stringify(fallbackValue);
693
+ return [{ type: "text", text: fallbackText }];
495
694
  }
496
695
  function toToolError(error) {
497
696
  if (error instanceof ToolError) {
@@ -524,7 +723,7 @@ function createResolvedMCPServer(root, options) {
524
723
  validateToolArguments: false
525
724
  });
526
725
  for (const tool of tools) {
527
- server.tool(tool.name, tool.description, tool.inputSchema, async (argumentsValue) => {
726
+ const handler = async (argumentsValue) => {
528
727
  let params;
529
728
  let secrets;
530
729
  try {
@@ -548,6 +747,13 @@ function createResolvedMCPServer(root, options) {
548
747
  if (isHumanInLoopPending(result)) {
549
748
  return renderPendingApproval(result);
550
749
  }
750
+ if (tool.resultSchema !== undefined) {
751
+ const structuredContent = validateCommandResult(tool.resultSchema, result, casing);
752
+ return {
753
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }],
754
+ structuredContent
755
+ };
756
+ }
551
757
  return toToolContent(result);
552
758
  }
553
759
  catch (error) {
@@ -566,7 +772,13 @@ function createResolvedMCPServer(root, options) {
566
772
  });
567
773
  throw toToolError(error);
568
774
  }
569
- });
775
+ };
776
+ if (tool.outputSchema === undefined) {
777
+ server.tool(tool.name, tool.description, tool.inputSchema, handler);
778
+ }
779
+ else {
780
+ server.tool(tool.name, tool.description, tool.inputSchema, handler, tool.outputSchema);
781
+ }
570
782
  }
571
783
  return {
572
784
  ...server,
@@ -1,8 +1,15 @@
1
1
  export function isValidNumberSchemaValue(value, schema) {
2
2
  return (typeof value === "number" &&
3
3
  Number.isFinite(value) &&
4
- (schema.jsonType !== "integer" || Number.isInteger(value)));
4
+ (schema.jsonType !== "integer" || Number.isInteger(value)) &&
5
+ (schema.minimum === undefined || value >= schema.minimum) &&
6
+ (schema.maximum === undefined || value <= schema.maximum));
5
7
  }
6
8
  export function getExpectedNumberDescription(schema) {
7
- return schema.jsonType === "integer" ? "an integer" : "a number";
9
+ const type = schema.jsonType === "integer" ? "an integer" : "a number";
10
+ const bounds = [
11
+ schema.minimum === undefined ? undefined : `greater than or equal to ${schema.minimum}`,
12
+ schema.maximum === undefined ? undefined : `less than or equal to ${schema.maximum}`
13
+ ].filter((bound) => bound !== undefined);
14
+ return bounds.length === 0 ? type : `${type} ${bounds.join(" and ")}`;
8
15
  }
@@ -11,7 +11,7 @@ export const claudeCodeAgent = {
11
11
  CLAUDE_CODE_ENABLE_TELEMETRY: "1"
12
12
  }
13
13
  },
14
- configPath: "~/.claude/settings.json",
14
+ configPath: "~/.claude.json",
15
15
  branding: {
16
16
  colors: {
17
17
  dark: "#C15F3C",
@@ -3,7 +3,12 @@ export const claudeDesktopAgent = {
3
3
  name: "claude-desktop",
4
4
  label: "Claude Desktop",
5
5
  summary: "Anthropic's official desktop application for Claude",
6
- configPath: "~/.claude/settings.json",
6
+ configPath: "~/.config/Claude/claude_desktop_config.json",
7
+ configPaths: {
8
+ darwin: "~/Library/Application Support/Claude/claude_desktop_config.json",
9
+ linux: "~/.config/Claude/claude_desktop_config.json",
10
+ win32: "~/AppData/Roaming/Claude/claude_desktop_config.json"
11
+ },
7
12
  branding: {
8
13
  colors: {
9
14
  dark: "#D97757",
@@ -5,7 +5,7 @@ export const cursorAgent = {
5
5
  label: "Cursor",
6
6
  summary: "Cursor's CLI coding agent.",
7
7
  binaryName: "cursor-agent",
8
- configPath: "~/.cursor/cli-config.json",
8
+ configPath: "~/.cursor/mcp.json",
9
9
  branding: {
10
10
  colors: {
11
11
  dark: "#FFFFFF",
@@ -6,7 +6,7 @@ export const kimiAgent = {
6
6
  aliases: ["kimi-cli"],
7
7
  binaryName: "kimi",
8
8
  apiShapes: ["openai-chat-completions"],
9
- configPath: "~/.kimi/config.toml",
9
+ configPath: "~/.kimi/mcp.json",
10
10
  branding: {
11
11
  colors: {
12
12
  dark: "#7B68EE",
@@ -10,7 +10,7 @@ export const openCodeAgent = {
10
10
  OPENCODE_CONFIG_CONTENT: '{"experimental":{"openTelemetry":true}}'
11
11
  }
12
12
  },
13
- configPath: "~/.config/opencode/config.json",
13
+ configPath: "~/.config/opencode/opencode.json",
14
14
  branding: {
15
15
  colors: {
16
16
  dark: "#4A4F55",
@@ -2,24 +2,33 @@ import { resolveAgentId } from "./registry.js";
2
2
  function getOwnModel(specifier) {
3
3
  return Object.prototype.hasOwnProperty.call(specifier, "model") ? specifier.model : undefined;
4
4
  }
5
+ function requireNonBlank(value, field) {
6
+ const trimmed = value.trim();
7
+ if (trimmed.length === 0) {
8
+ throw new TypeError(`${field} must not be empty`);
9
+ }
10
+ return trimmed;
11
+ }
5
12
  export function parseAgentSpecifier(input) {
6
13
  const colonIndex = input.indexOf(":");
7
14
  if (colonIndex === -1) {
8
- return { agent: input.trim() };
15
+ return { agent: requireNonBlank(input, "agent") };
9
16
  }
10
- const agent = input.slice(0, colonIndex).trim();
17
+ const agent = requireNonBlank(input.slice(0, colonIndex), "agent");
11
18
  const model = input.slice(colonIndex + 1).trim();
12
19
  return {
13
20
  agent,
14
- ...(model.length > 0 ? { model } : {}),
21
+ ...(model.length > 0 ? { model } : {})
15
22
  };
16
23
  }
17
24
  export function formatAgentSpecifier(specifier) {
25
+ const agent = requireNonBlank(specifier.agent, "agent");
18
26
  const model = getOwnModel(specifier);
19
- if (model) {
20
- return `${specifier.agent}:${model}`;
27
+ const normalizedModel = model?.trim();
28
+ if (normalizedModel) {
29
+ return `${agent}:${normalizedModel}`;
21
30
  }
22
- return specifier.agent;
31
+ return agent;
23
32
  }
24
33
  export function normalizeAgentId(input) {
25
34
  const specifier = parseAgentSpecifier(input.trim());
@@ -14,6 +14,11 @@ export interface AgentDefinition {
14
14
  readonly apiShapes?: readonly ApiShapeId[];
15
15
  readonly otelCapture?: OtelCaptureDefinition;
16
16
  configPath: string;
17
+ readonly configPaths?: {
18
+ readonly darwin: string;
19
+ readonly linux: string;
20
+ readonly win32: string;
21
+ };
17
22
  branding: {
18
23
  colors: {
19
24
  dark: string;
@@ -15,6 +15,32 @@ export class UnsupportedAgentError extends Error {
15
15
  function isConfigObject(value) {
16
16
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
17
17
  }
18
+ function assertNonEmptyString(value, message) {
19
+ if (typeof value !== "string" || value.trim().length === 0) {
20
+ throw new Error(message);
21
+ }
22
+ }
23
+ function assertHttpUrl(value) {
24
+ assertNonEmptyString(value, "MCP HTTP URL must be a valid http or https URL.");
25
+ let parsed;
26
+ try {
27
+ parsed = new URL(value);
28
+ }
29
+ catch {
30
+ throw new Error("MCP HTTP URL must be a valid http or https URL.");
31
+ }
32
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
33
+ throw new Error("MCP HTTP URL must be a valid http or https URL.");
34
+ }
35
+ }
36
+ function validateServerEntry(server) {
37
+ assertNonEmptyString(server.name, "MCP server name must be a non-empty string.");
38
+ if (server.config.transport === "stdio") {
39
+ assertNonEmptyString(server.config.command, "MCP stdio command must be a non-empty string.");
40
+ return;
41
+ }
42
+ assertHttpUrl(server.config.url);
43
+ }
18
44
  function resolveServerMap(document, configKey) {
19
45
  const value = document[configKey];
20
46
  if (value === undefined) {
@@ -32,12 +58,15 @@ export async function configure(agentId, server, options) {
32
58
  if (!isSupported(agentId)) {
33
59
  throw new UnsupportedAgentError(agentId);
34
60
  }
61
+ validateServerEntry(server);
35
62
  const config = getAgentConfig(agentId);
36
63
  const configPath = resolveConfigPath(config, options.platform);
37
64
  const shapeTransformer = getShapeTransformer(config.shape);
38
65
  const shaped = shapeTransformer(server);
66
+ const enabledServer = { ...server, enabled: true };
67
+ const enabledShaped = shapeTransformer(enabledServer);
39
68
  if (shaped === undefined) {
40
- await unconfigure(agentId, server.name, options);
69
+ await unconfigure(agentId, enabledServer, options);
41
70
  return;
42
71
  }
43
72
  const configDir = getConfigDirectory(configPath);
@@ -57,10 +86,13 @@ export async function configure(agentId, server, options) {
57
86
  ? servers[server.name]
58
87
  : undefined;
59
88
  const shapedServer = shaped;
89
+ const enabledShapedServer = enabledShaped;
60
90
  if (existingServer !== undefined && isDeepStrictEqual(existingServer, shapedServer)) {
61
91
  return { changed: false, content: document };
62
92
  }
63
- if (existingServer !== undefined && server.enabled !== false) {
93
+ if (existingServer !== undefined &&
94
+ (enabledShapedServer === undefined ||
95
+ !isDeepStrictEqual(existingServer, enabledShapedServer))) {
64
96
  throw new Error(`MCP server "${server.name}" already exists with different configuration in ${configPath}.`);
65
97
  }
66
98
  const newServers = {
@@ -98,7 +130,8 @@ export async function unconfigure(agentId, server, options) {
98
130
  if (!Object.hasOwn(servers, serverName)) {
99
131
  return { changed: false, content: document };
100
132
  }
101
- if (expectedServer !== undefined && !isDeepStrictEqual(servers[serverName], expectedServer)) {
133
+ if (expectedServer !== undefined &&
134
+ !isDeepStrictEqual(servers[serverName], expectedServer)) {
102
135
  return { changed: false, content: document };
103
136
  }
104
137
  const newServers = { ...servers };
@@ -0,0 +1,35 @@
1
+ # @poe-code/frontmatter
2
+
3
+ Shared YAML frontmatter parsing for poe-code packages.
4
+
5
+ ## API
6
+
7
+ ```ts
8
+ import {
9
+ FrontmatterParseError,
10
+ parseFrontmatter,
11
+ parseFrontmatterDocument,
12
+ stringifyFrontmatter
13
+ } from "@poe-code/frontmatter";
14
+ ```
15
+
16
+ - `parseFrontmatter(source)` returns `{ frontmatter, body }`.
17
+ - `parseFrontmatterDocument(source)` returns `{ frontmatter, body, errors, lineCounter }` for callers that need YAML diagnostics.
18
+ - `stringifyFrontmatter(frontmatter, body)` writes `---` fences, YAML, and the body.
19
+ - `FrontmatterParseError` is thrown for malformed frontmatter, invalid YAML, non-object frontmatter, and stringify failures.
20
+
21
+ When no leading frontmatter block exists, parsing returns `{ frontmatter: {}, body: source }`.
22
+ The returned `body` is sliced from the original input and is otherwise byte-for-byte unchanged.
23
+ YAML mappings that contain `__proto__` keep it as an own property without changing the object's prototype.
24
+
25
+ Duplicate mapping keys use YAML's last-wins behavior by default, matching the
26
+ legacy frontmatter readers. Callers that need strict YAML mappings can pass
27
+ `{ uniqueKeys: true }` to `parseFrontmatter` or `parseFrontmatterDocument`.
28
+
29
+ ## Environment
30
+
31
+ This package reads no environment variables.
32
+
33
+ ## Config
34
+
35
+ This package has no configuration options.
@@ -0,0 +1,8 @@
1
+ export type FrontmatterBlock = {
2
+ raw: string;
3
+ body: string;
4
+ };
5
+ export type SplitFrontmatterResult = FrontmatterBlock | {
6
+ body: string;
7
+ };
8
+ export declare function splitFrontmatterBlock(source: string): SplitFrontmatterResult;
@@ -0,0 +1,72 @@
1
+ export function splitFrontmatterBlock(source) {
2
+ const content = source.startsWith("\uFEFF") ? source.slice(1) : source;
3
+ const opening = readOpeningFence(content);
4
+ if (opening === undefined) {
5
+ return { body: source };
6
+ }
7
+ const closing = findClosingFence(content, opening.next);
8
+ if (closing === undefined) {
9
+ throw new Error("Missing YAML frontmatter end delimiter (---).");
10
+ }
11
+ return {
12
+ raw: content.slice(opening.next, closing.index),
13
+ body: content.slice(closing.end + closing.lineBreakLength)
14
+ };
15
+ }
16
+ function readOpeningFence(source) {
17
+ if (!source.startsWith("---")) {
18
+ return undefined;
19
+ }
20
+ const lineEnd = findLineEnd(source, 0);
21
+ if (lineEnd.lineBreakLength === 0 || source.slice(0, lineEnd.index) !== "---") {
22
+ return undefined;
23
+ }
24
+ return { next: lineEnd.index + lineEnd.lineBreakLength };
25
+ }
26
+ function findClosingFence(source, start) {
27
+ let lineStart = start;
28
+ while (lineStart <= source.length) {
29
+ const lineEnd = findLineEnd(source, lineStart);
30
+ if (isClosingFenceLine(source.slice(lineStart, lineEnd.index))) {
31
+ return {
32
+ index: lineStart,
33
+ end: lineEnd.index,
34
+ lineBreakLength: lineEnd.lineBreakLength
35
+ };
36
+ }
37
+ if (lineEnd.lineBreakLength === 0) {
38
+ break;
39
+ }
40
+ lineStart = lineEnd.index + lineEnd.lineBreakLength;
41
+ }
42
+ return undefined;
43
+ }
44
+ function isClosingFenceLine(line) {
45
+ if (!line.startsWith("---")) {
46
+ return false;
47
+ }
48
+ for (let index = 3; index < line.length; index += 1) {
49
+ const character = line[index];
50
+ if (character !== " " && character !== "\t") {
51
+ return false;
52
+ }
53
+ }
54
+ return true;
55
+ }
56
+ function findLineEnd(source, start) {
57
+ let index = start;
58
+ while (index < source.length) {
59
+ const character = source[index];
60
+ if (character === "\n") {
61
+ return { index, lineBreakLength: 1 };
62
+ }
63
+ if (character === "\r") {
64
+ return {
65
+ index,
66
+ lineBreakLength: source[index + 1] === "\n" ? 2 : 1
67
+ };
68
+ }
69
+ index += 1;
70
+ }
71
+ return { index: source.length, lineBreakLength: 0 };
72
+ }
@@ -0,0 +1,3 @@
1
+ export { splitFrontmatterBlock, type FrontmatterBlock, type SplitFrontmatterResult } from "./fences.js";
2
+ export { FrontmatterParseError, parseFrontmatter, parseFrontmatterDocument, type ParsedFrontmatter, type ParsedFrontmatterDocument, type ParseFrontmatterOptions } from "./parse.js";
3
+ export { stringifyFrontmatter } from "./stringify.js";
@@ -0,0 +1,3 @@
1
+ export { splitFrontmatterBlock } from "./fences.js";
2
+ export { FrontmatterParseError, parseFrontmatter, parseFrontmatterDocument } from "./parse.js";
3
+ export { stringifyFrontmatter } from "./stringify.js";
@@ -0,0 +1,20 @@
1
+ import { LineCounter } from "yaml";
2
+ export interface ParsedFrontmatter {
3
+ frontmatter: Record<string, unknown>;
4
+ body: string;
5
+ }
6
+ export interface ParsedFrontmatterDocument extends ParsedFrontmatter {
7
+ errors: readonly {
8
+ message: string;
9
+ pos?: [number, number];
10
+ }[];
11
+ lineCounter: LineCounter;
12
+ }
13
+ export interface ParseFrontmatterOptions {
14
+ uniqueKeys?: boolean;
15
+ }
16
+ export declare class FrontmatterParseError extends Error {
17
+ constructor(message: string);
18
+ }
19
+ export declare function parseFrontmatter(source: string, options?: ParseFrontmatterOptions): ParsedFrontmatter;
20
+ export declare function parseFrontmatterDocument(source: string, options?: ParseFrontmatterOptions): ParsedFrontmatterDocument;