@contractkit/plugin-typescript 0.34.0 → 0.34.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/llms.txt CHANGED
@@ -82,6 +82,20 @@ Options worth knowing:
82
82
  schema for argument validation and `z.toJSONSchema`. It falls back to `server.output.types` (when
83
83
  `server.zod`) or the `zod` sub-config's output.
84
84
 
85
+ ## Wiring the MCP tools
86
+
87
+ `mcp.tools.ts` exports `registerMcpTools(container)`, which **builds and returns** the
88
+ `McpToolHandlerMap`. It registers nothing: `register` belongs to InjectKit's `Registry`
89
+ (composition phase), while a `Container` (resolution phase) only resolves. Bind the aggregator from
90
+ a factory, which is also what supplies the `Container` it needs:
91
+
92
+ ```typescript
93
+ registry.register(McpToolHandlerMap).useFactory(registerMcpTools).asSingleton();
94
+ ```
95
+
96
+ The emitted tool classes are not registered for you either, so register each one on the same
97
+ `Registry` before the aggregator resolves it.
98
+
85
99
  ## Programmatic use
86
100
 
87
101
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contractkit/plugin-typescript",
3
- "version": "0.34.0",
3
+ "version": "0.34.1",
4
4
  "description": "ContractKit built-in plugin: TypeScript codegen (SDK clients, Koa routers, Zod schemas, plain types)",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -372,7 +372,10 @@ function renderToolClass(plan: ToolPlan, file: string, options: McpCodegenOption
372
372
  const isVoid = !primaryResponseBody(op);
373
373
  const structured = !!outExpr;
374
374
 
375
- lines.push(' async handle(args: Record<string, unknown>, _context: McpToolContext): Promise<CallToolResult> {');
375
+ // No args to destructure means the parameter goes unread, which trips no-unused-vars in
376
+ // consumers that lint generated output; the leading underscore opts it out.
377
+ const argsParam = destructure.length > 0 ? 'args' : '_args';
378
+ lines.push(` async handle(${argsParam}: Record<string, unknown>, _context: McpToolContext): Promise<CallToolResult> {`);
376
379
  if (destructure.length > 0) {
377
380
  lines.push(` const { ${destructure.join(', ')} } = await parseAndValidate(args, ${argsConstName});`);
378
381
  }
@@ -477,11 +480,19 @@ export function generateMcpAggregator(entries: McpAggregatorEntry[]): string {
477
480
  lines.push(`import { McpToolHandlerMap } from '@maroonedsoftware/mcp';`);
478
481
  for (const e of sorted) lines.push(`import { ${e.registerFn} } from '${e.importPath}';`);
479
482
  lines.push('');
480
- lines.push('/** Build + register the MCP tool catalog. Call once at startup. */');
483
+ lines.push('/**');
484
+ lines.push(' * Build the MCP tool catalog.');
485
+ lines.push(' *');
486
+ lines.push(' * Bind it to the `McpToolHandlerMap` token from a factory, which is what supplies the');
487
+ lines.push(' * `Container` needed to resolve each handler:');
488
+ lines.push(' *');
489
+ lines.push(' * ```ts');
490
+ lines.push(' * registry.register(McpToolHandlerMap).useFactory(registerMcpTools).asSingleton();');
491
+ lines.push(' * ```');
492
+ lines.push(' */');
481
493
  lines.push('export function registerMcpTools(container: Container): McpToolHandlerMap {');
482
494
  lines.push(' const map = new McpToolHandlerMap();');
483
495
  for (const e of sorted) lines.push(` ${e.registerFn}(map, container);`);
484
- lines.push(' container.register(McpToolHandlerMap, { useValue: map });');
485
496
  lines.push(' return map;');
486
497
  lines.push('}');
487
498
  return lines.join('\n') + '\n';
@@ -493,7 +504,7 @@ export function generateMcpRouter(options: { path?: string } = {}): string {
493
504
  return `import { ServerKitRouter, bodyParserMiddleware, requireSignature } from '@maroonedsoftware/koa';
494
505
  import { McpDispatcher, createMcpRequestContext, MCP_AUTH_POLICY } from '@maroonedsoftware/mcp';
495
506
 
496
- /** Mount the MCP endpoint onto a ServerKit router. Call \`registerMcpTools(container)\` at startup. */
507
+ /** Mount the MCP endpoint onto a ServerKit router. Bind \`registerMcpTools\` to the \`McpToolHandlerMap\` token. */
497
508
  export function mountMcp(router: ReturnType<typeof ServerKitRouter>): void {
498
509
  router.post('${path}', bodyParserMiddleware(['json']), requireSignature('mcp', { policy: MCP_AUTH_POLICY }), async (ctx) => {
499
510
  const dispatcher = ctx.container.get(McpDispatcher);
@@ -141,6 +141,16 @@ describe('generateMcpFile', () => {
141
141
  expect(out).toContain('const CreatePaymentArgs = z.object({ body: PaymentInput });');
142
142
  expect(out).toContain('const { body } = await parseAndValidate(args, CreatePaymentArgs);');
143
143
  });
144
+
145
+ it('underscores the args param when the op takes no arguments', () => {
146
+ const root = opRoot([opRoute('/health', [opOperation('get', { mcp: true, responses: [opResponse(200, 'Health', 'application/json')] })])]);
147
+ const out = generateMcpFile(root);
148
+ // Nothing destructures `args` here, so an un-prefixed name trips no-unused-vars in
149
+ // consumers that lint generated output.
150
+ expect(out).toContain('async handle(_args: Record<string, unknown>, _context: McpToolContext)');
151
+ expect(out).not.toContain('async handle(args:');
152
+ expect(out).not.toContain('parseAndValidate');
153
+ });
144
154
  });
145
155
 
146
156
  describe('service call + result', () => {
@@ -220,7 +230,13 @@ describe('generateMcpAggregator', () => {
220
230
  expect(out).toContain('const map = new McpToolHandlerMap();');
221
231
  expect(out).toContain('registerPaymentsMcpTools(map, container);');
222
232
  expect(out).toContain('registerUsersMcpTools(map, container);');
223
- expect(out).toContain('container.register(McpToolHandlerMap, { useValue: map });');
233
+ expect(out).toContain('return map;');
234
+ });
235
+
236
+ it('only builds the map — Container has no register, that belongs to Registry', () => {
237
+ const out = generateMcpAggregator([{ registerFn: 'registerPaymentsMcpTools', importPath: './payments.mcp.js' }]);
238
+ expect(out).not.toContain('container.register');
239
+ expect(out).toContain('registry.register(McpToolHandlerMap).useFactory(registerMcpTools).asSingleton();');
224
240
  });
225
241
  });
226
242
 
@@ -233,6 +249,12 @@ describe('generateMcpRouter', () => {
233
249
  expect(out).toContain("dispatcher.sessionMode === 'stateful'");
234
250
  });
235
251
 
252
+ it('tells consumers to bind the aggregator rather than call it at startup', () => {
253
+ const out = generateMcpRouter();
254
+ expect(out).toContain('Bind `registerMcpTools` to the `McpToolHandlerMap` token.');
255
+ expect(out).not.toContain('Call `registerMcpTools(container)` at startup');
256
+ });
257
+
236
258
  it('parses the body before verifying the signature', () => {
237
259
  const out = generateMcpRouter({ path: '/mcp' });
238
260
  // `requireSignature` HMACs `ctx.rawBody`, which only `bodyParserMiddleware` populates,