@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/.turbo/turbo-build$colon$ci.log +4 -4
- package/.turbo/turbo-test$colon$ci.log +15 -15
- package/CHANGELOG.md +18 -0
- package/dist/codegen-mcp.d.ts.map +1 -1
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/llms.txt +14 -0
- package/package.json +1 -1
- package/src/codegen-mcp.ts +15 -4
- package/tests/codegen-mcp.test.ts +23 -1
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
package/src/codegen-mcp.ts
CHANGED
|
@@ -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
|
-
|
|
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('/**
|
|
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.
|
|
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('
|
|
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,
|