@contractkit/plugin-typescript 0.34.0 → 0.35.0

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/dist/index.js CHANGED
@@ -1453,6 +1453,110 @@ __name(pascalToDotCase, "pascalToDotCase");
1453
1453
  // src/codegen-operation.ts
1454
1454
  import { resolveModifiers, resolveSecurity, SECURITY_NONE, classifyContentType, emittedResponses, PATH_PARAM_RE_G, toIdentifier } from "@contractkit/core";
1455
1455
  import { basename, dirname as dirname3, relative as relative3 } from "path";
1456
+
1457
+ // src/server-framework-koa.ts
1458
+ var KOA_RUNTIME_MODULE = "@maroonedsoftware/koa";
1459
+ var KOA_RUNTIME_SYMBOLS = [
1460
+ "ServerKitRouter",
1461
+ "bodyParserMiddleware",
1462
+ "requirePolicy",
1463
+ "requireSignature"
1464
+ ];
1465
+ var KOA_SERVER_FRAMEWORK = {
1466
+ name: "koa",
1467
+ imports(uses) {
1468
+ const symbols = KOA_RUNTIME_SYMBOLS.filter(uses);
1469
+ return symbols.length > 0 ? [
1470
+ `import { ${symbols.join(", ")} } from '${KOA_RUNTIME_MODULE}';`
1471
+ ] : [];
1472
+ },
1473
+ routerDeclaration(routerName) {
1474
+ return `export const ${routerName} = ServerKitRouter();`;
1475
+ },
1476
+ pathParam(identifier) {
1477
+ return `:${identifier}`;
1478
+ },
1479
+ routeOpen(routerName, method, path, middlewares) {
1480
+ const middlewareStr = middlewares.length > 0 ? `, ${middlewares.join(", ")},` : ",";
1481
+ return `${routerName}.${method}('${path}'${middlewareStr} async ctx => {`;
1482
+ },
1483
+ routeClose() {
1484
+ return [
1485
+ "});"
1486
+ ];
1487
+ },
1488
+ middleware: {
1489
+ policy(args) {
1490
+ return `requirePolicy(${args})`;
1491
+ },
1492
+ bodyParser(tokensExpr) {
1493
+ return `bodyParserMiddleware([${tokensExpr}])`;
1494
+ },
1495
+ signature(args) {
1496
+ return `requireSignature(${args})`;
1497
+ }
1498
+ },
1499
+ request: {
1500
+ params: "ctx.params",
1501
+ query: "ctx.query",
1502
+ headers: "ctx.headers",
1503
+ // Not `ctx.request.body`: the ServerKit body parser drains the stream and writes its result
1504
+ // here, and in Koa `ctx.body` is the *response* body.
1505
+ parsedBody: "ctx.parsedBody",
1506
+ // Koa strips the parameters off `Content-Type` for this accessor already.
1507
+ contentType: "ctx.request.type"
1508
+ },
1509
+ resolveService(className) {
1510
+ return `ctx.container.get(${className})`;
1511
+ },
1512
+ response: {
1513
+ status(expr) {
1514
+ return `ctx.status = ${expr};`;
1515
+ },
1516
+ header(name, valueExpr) {
1517
+ return `ctx.set('${name}', ${valueExpr});`;
1518
+ },
1519
+ type(expr) {
1520
+ return `ctx.type = ${expr};`;
1521
+ },
1522
+ send(bodyExpr) {
1523
+ return bodyExpr === void 0 ? [] : [
1524
+ `ctx.body = ${bodyExpr};`
1525
+ ];
1526
+ },
1527
+ caseEnd() {
1528
+ return [
1529
+ "break;"
1530
+ ];
1531
+ }
1532
+ },
1533
+ mcpRouter({ path }) {
1534
+ return `import { ServerKitRouter, bodyParserMiddleware, requireSignature } from '${KOA_RUNTIME_MODULE}';
1535
+ import { McpDispatcher, createMcpRequestContext, MCP_AUTH_POLICY } from '@maroonedsoftware/mcp';
1536
+
1537
+ /** Mount the MCP endpoint onto a ServerKit router. Bind \`registerMcpTools\` to the \`McpToolHandlerMap\` token. */
1538
+ export function mountMcp(router: ReturnType<typeof ServerKitRouter>): void {
1539
+ router.post('${path}', bodyParserMiddleware(['json']), requireSignature('mcp', { policy: MCP_AUTH_POLICY }), async (ctx) => {
1540
+ const dispatcher = ctx.container.get(McpDispatcher);
1541
+ const context = createMcpRequestContext({ requestId: ctx.requestId, logger: ctx.logger });
1542
+ if (dispatcher.sessionMode === 'stateful') {
1543
+ ctx.respond = false;
1544
+ await dispatcher.dispatchStateful(
1545
+ { req: ctx.req, res: ctx.res, body: ctx.parsedBody, sessionId: ctx.get('mcp-session-id') },
1546
+ context,
1547
+ );
1548
+ } else {
1549
+ const response = await dispatcher.dispatch(JSON.parse(String(ctx.rawBody)), context);
1550
+ if (response) ctx.body = response;
1551
+ else ctx.status = 202; // a notification \u2014 nothing to return
1552
+ }
1553
+ });
1554
+ }
1555
+ `;
1556
+ }
1557
+ };
1558
+
1559
+ // src/codegen-operation.ts
1456
1560
  function bodyParserToken(contentType) {
1457
1561
  switch (classifyContentType(contentType)) {
1458
1562
  case "urlencoded":
@@ -1525,6 +1629,11 @@ function bodyTypesStructurallyEqual(a, b) {
1525
1629
  }
1526
1630
  __name(bodyTypesStructurallyEqual, "bodyTypesStructurallyEqual");
1527
1631
  function generateOp(root, options = {}) {
1632
+ const resolved = {
1633
+ ...options,
1634
+ framework: options.framework ?? KOA_SERVER_FRAMEWORK
1635
+ };
1636
+ const framework = resolved.framework;
1528
1637
  const types = collectTypes(root, options.modelsWithInput, options.modelsWithOutput);
1529
1638
  const services = collectServices(root);
1530
1639
  const routerName = deriveRouterName(root.file);
@@ -1533,13 +1642,13 @@ function generateOp(root, options = {}) {
1533
1642
  lines.push("/**");
1534
1643
  lines.push(` * generated from ${sourceLink(basename(root.file), options.outPath, root.file)}`);
1535
1644
  lines.push("*/");
1536
- lines.push(`export const ${routerName} = ServerKitRouter();`);
1645
+ lines.push(framework.routerDeclaration(routerName));
1537
1646
  lines.push("");
1538
1647
  const includeInternal = options.includeInternal ?? true;
1539
1648
  for (const route of root.routes) {
1540
1649
  for (const op of route.operations) {
1541
1650
  if (!includeInternal && resolveModifiers(route, op).includes("internal")) continue;
1542
- lines.push(...generateHandler(route, op, root, options));
1651
+ lines.push(...generateHandler(route, op, root, resolved));
1543
1652
  lines.push("");
1544
1653
  }
1545
1654
  }
@@ -1574,15 +1683,7 @@ function generateOp(root, options = {}) {
1574
1683
  ].join("\n");
1575
1684
  const uses = /* @__PURE__ */ __name((symbol) => new RegExp(`\\b${symbol}\\b`).test(generated), "uses");
1576
1685
  const body = [];
1577
- const koaImports = [
1578
- "ServerKitRouter",
1579
- "bodyParserMiddleware",
1580
- "requirePolicy",
1581
- "requireSignature"
1582
- ].filter(uses);
1583
- if (koaImports.length > 0) {
1584
- body.push(`import { ${koaImports.join(", ")} } from '@maroonedsoftware/koa';`);
1585
- }
1686
+ body.push(...framework.imports(uses));
1586
1687
  for (const svc of services.filter(uses)) {
1587
1688
  const modulePath = root.services?.[svc] ?? root.meta[svc] ?? deriveModulePath(svc, options.servicePathTemplate);
1588
1689
  body.push(`import { ${svc} } from '${modulePath}';`);
@@ -1626,6 +1727,7 @@ function generateHandler(route, op, root, options) {
1626
1727
  const file = root.file;
1627
1728
  const outPath = options.outPath;
1628
1729
  const modelsWithInput = options.modelsWithInput;
1730
+ const framework = options.framework;
1629
1731
  lines.push("/**");
1630
1732
  const desc = op.description ?? route.description;
1631
1733
  if (desc) {
@@ -1641,7 +1743,7 @@ function generateHandler(route, op, root, options) {
1641
1743
  if (mods.includes("deprecated")) lines.push(` * @deprecated`);
1642
1744
  lines.push("*/");
1643
1745
  const method = op.method;
1644
- const path = route.path.replace(PATH_PARAM_RE_G, (_m, name) => `:${toIdentifier(name)}`);
1746
+ const path = route.path.replace(PATH_PARAM_RE_G, (_m, name) => framework.pathParam(toIdentifier(name)));
1645
1747
  const bodies = op.request?.bodies ?? [];
1646
1748
  const hasBody = bodies.length > 0;
1647
1749
  const isSingleMultipart = bodies.length === 1 && bodies[0].contentType === "multipart/form-data";
@@ -1649,42 +1751,41 @@ function generateHandler(route, op, root, options) {
1649
1751
  if (effectiveSecurity !== SECURITY_NONE) {
1650
1752
  const policy = effectiveSecurity?.policy;
1651
1753
  const args = policy === void 0 ? "" : policy === false ? "{ policy: false }" : `{ policy: '${policy}' }`;
1652
- middlewares.push(`requirePolicy(${args})`);
1754
+ middlewares.push(framework.middleware.policy(args));
1653
1755
  }
1654
1756
  if (hasBody) {
1655
1757
  const parserTokens = Array.from(new Set(bodies.map((b) => bodyParserToken(b.contentType))));
1656
1758
  const tokensExpr = parserTokens.map((t) => `'${t}'`).join(", ");
1657
- middlewares.push(`bodyParserMiddleware([${tokensExpr}])`);
1759
+ middlewares.push(framework.middleware.bodyParser(tokensExpr));
1658
1760
  }
1659
1761
  if (op.signature) {
1660
1762
  const sigArgs = op.signaturePolicy ? `'${escapeSingleQuoted(op.signature)}', { policy: '${escapeSingleQuoted(op.signaturePolicy)}' }` : `'${escapeSingleQuoted(op.signature)}'`;
1661
- middlewares.push(`requireSignature(${sigArgs})`);
1763
+ middlewares.push(framework.middleware.signature(sigArgs));
1662
1764
  }
1663
- const middlewareStr = middlewares.length > 0 ? `, ${middlewares.join(", ")},` : ",";
1664
- lines.push(`${deriveRouterName(file)}.${method}('${path}'${middlewareStr} async ctx => {`);
1665
- lines.push(...generateParamValidation(route.params, "ctx.params", "params", route.paramsMode ?? "strict", "", modelsWithInput));
1666
- lines.push(...generateParamValidation(op.query, "ctx.query", "query", op.queryMode ?? "strict", "", modelsWithInput));
1667
- lines.push(...generateParamValidation(op.headers, "ctx.headers", "headers", op.headersMode ?? "strip", "", modelsWithInput));
1765
+ lines.push(framework.routeOpen(deriveRouterName(file), method, path, middlewares));
1766
+ lines.push(...generateParamValidation(route.params, "params", framework.request.params, route.paramsMode ?? "strict", "", modelsWithInput));
1767
+ lines.push(...generateParamValidation(op.query, "query", framework.request.query, op.queryMode ?? "strict", "", modelsWithInput));
1768
+ lines.push(...generateParamValidation(op.headers, "headers", framework.request.headers, op.headersMode ?? "strip", "", modelsWithInput));
1668
1769
  if (hasBody && op.request) {
1669
1770
  if (isSingleMultipart) {
1670
- lines.push(` const multipartBody = ctx.parsedBody as MultipartBody;`);
1771
+ lines.push(` const multipartBody = ${framework.request.parsedBody} as MultipartBody;`);
1671
1772
  lines.push("");
1672
1773
  } else if (bodies.length === 1) {
1673
- lines.push(` const body = await parseAndValidate(ctx.parsedBody, ${renderInputType(bodies[0].bodyType, modelsWithInput)});`);
1774
+ lines.push(` const body = await parseAndValidate(${framework.request.parsedBody}, ${renderInputType(bodies[0].bodyType, modelsWithInput)});`);
1674
1775
  lines.push("");
1675
1776
  } else if (bodies.every((b) => bodyTypesStructurallyEqual(b.bodyType, bodies[0].bodyType))) {
1676
- lines.push(` const body = await parseAndValidate(ctx.parsedBody, ${renderInputType(bodies[0].bodyType, modelsWithInput)});`);
1777
+ lines.push(` const body = await parseAndValidate(${framework.request.parsedBody}, ${renderInputType(bodies[0].bodyType, modelsWithInput)});`);
1677
1778
  lines.push("");
1678
1779
  } else {
1679
1780
  const annotation = bodies.map((b) => b.contentType === "multipart/form-data" ? "MultipartBody" : `z.infer<typeof ${renderInputType(b.bodyType, modelsWithInput)}>`).join(" | ");
1680
1781
  lines.push(` let body!: ${annotation};`);
1681
- lines.push(` switch (ctx.request.type) {`);
1782
+ lines.push(` switch (${framework.request.contentType}) {`);
1682
1783
  for (const b of bodies) {
1683
1784
  lines.push(` case '${b.contentType}':`);
1684
1785
  if (b.contentType === "multipart/form-data") {
1685
- lines.push(` body = ctx.parsedBody as MultipartBody;`);
1786
+ lines.push(` body = ${framework.request.parsedBody} as MultipartBody;`);
1686
1787
  } else {
1687
- lines.push(` body = await parseAndValidate(ctx.parsedBody, ${renderInputType(b.bodyType, modelsWithInput)});`);
1788
+ lines.push(` body = await parseAndValidate(${framework.request.parsedBody}, ${renderInputType(b.bodyType, modelsWithInput)});`);
1688
1789
  }
1689
1790
  lines.push(` break;`);
1690
1791
  }
@@ -1700,11 +1801,12 @@ function generateHandler(route, op, root, options) {
1700
1801
  } else {
1701
1802
  lines.push(...generateSingleStatusResult(emitted[0], op, serviceParts.className, call, options));
1702
1803
  }
1703
- lines.push(`});`);
1804
+ lines.push(...framework.routeClose());
1704
1805
  return lines;
1705
1806
  }
1706
1807
  __name(generateHandler, "generateHandler");
1707
1808
  function generateSingleStatusResult(resp, op, className, call, options) {
1809
+ const framework = options.framework;
1708
1810
  const lines = [];
1709
1811
  const bodies = resp ? resp.bodies : [];
1710
1812
  const respHeaders = resp?.headers ?? [];
@@ -1715,7 +1817,7 @@ function generateSingleStatusResult(resp, op, className, call, options) {
1715
1817
  const { annotation, prelude } = formatTypeAnnotation(bodies[0].bodyType, options.modelsWithOutput);
1716
1818
  if (prelude) lines.push(` ${prelude}`);
1717
1819
  bodySchema = responseBodySchema(bodies[0].bodyType, options, prelude ? "resultType" : void 0);
1718
- lines.push(` const service = ctx.container.get(${className});`);
1820
+ lines.push(` const service = ${framework.resolveService(className)};`);
1719
1821
  if (hasRespHeaders) {
1720
1822
  lines.push(` const result: { body: ${annotation}; headers: ${headersAnnotation} } = ${call};`);
1721
1823
  } else {
@@ -1729,10 +1831,10 @@ function generateSingleStatusResult(resp, op, className, call, options) {
1729
1831
  const { members, preludes } = rendered;
1730
1832
  bodySchema = rendered.bodySchema;
1731
1833
  for (const prelude of preludes) lines.push(` ${prelude}`);
1732
- lines.push(` const service = ctx.container.get(${className});`);
1834
+ lines.push(` const service = ${framework.resolveService(className)};`);
1733
1835
  lines.push(` const result: ${members.join(" | ")} = ${call};`);
1734
1836
  } else {
1735
- lines.push(` const service = ctx.container.get(${className});`);
1837
+ lines.push(` const service = ${framework.resolveService(className)};`);
1736
1838
  if (hasRespHeaders) {
1737
1839
  lines.push(` const result: { headers: ${headersAnnotation} } = ${call};`);
1738
1840
  } else {
@@ -1740,19 +1842,22 @@ function generateSingleStatusResult(resp, op, className, call, options) {
1740
1842
  }
1741
1843
  }
1742
1844
  lines.push("");
1743
- lines.push(` ctx.status = ${resp?.statusCode ?? 204};`);
1744
- lines.push(...headerSetLines(respHeaders, " "));
1845
+ lines.push(` ${framework.response.status(String(resp?.statusCode ?? 204))}`);
1846
+ lines.push(...headerSetLines(respHeaders, " ", framework));
1745
1847
  if (bodies.length === 1) {
1746
- lines.push(` ctx.type = '${bodies[0].contentType}';`);
1747
- lines.push(` ctx.body = ${responseBodyExpr(hasRespHeaders ? "result.body" : "result", bodySchema)};`);
1848
+ lines.push(` ${framework.response.type(`'${bodies[0].contentType}'`)}`);
1849
+ lines.push(...indent(framework.response.send(responseBodyExpr(hasRespHeaders ? "result.body" : "result", bodySchema)), " "));
1748
1850
  } else if (bodies.length > 1) {
1749
- lines.push(` ctx.type = result.contentType;`);
1750
- lines.push(` ctx.body = ${responseBodyExpr("result.body", bodySchema)};`);
1851
+ lines.push(` ${framework.response.type("result.contentType")}`);
1852
+ lines.push(...indent(framework.response.send(responseBodyExpr("result.body", bodySchema)), " "));
1853
+ } else {
1854
+ lines.push(...indent(framework.response.send(void 0), " "));
1751
1855
  }
1752
1856
  return lines;
1753
1857
  }
1754
1858
  __name(generateSingleStatusResult, "generateSingleStatusResult");
1755
1859
  function generateMultiStatusResult(emitted, className, call, options) {
1860
+ const framework = options.framework;
1756
1861
  const lines = [];
1757
1862
  const members = [];
1758
1863
  const preludes = [];
@@ -1767,21 +1872,23 @@ function generateMultiStatusResult(emitted, className, call, options) {
1767
1872
  bodySchemas.set(resp.statusCode, rendered.bodySchema);
1768
1873
  }
1769
1874
  for (const prelude of preludes) lines.push(` ${prelude}`);
1770
- lines.push(` const service = ctx.container.get(${className});`);
1875
+ lines.push(` const service = ${framework.resolveService(className)};`);
1771
1876
  lines.push(` const result:`);
1772
1877
  for (const member of members) lines.push(` | ${member}`);
1773
1878
  lines.push(` = ${call};`);
1774
1879
  lines.push("");
1775
- lines.push(` ctx.status = result.status;`);
1880
+ lines.push(` ${framework.response.status("result.status")}`);
1776
1881
  lines.push(` switch (result.status) {`);
1777
1882
  for (const resp of emitted) {
1778
1883
  lines.push(` case ${resp.statusCode}:`);
1779
- lines.push(...headerSetLines(resp.headers ?? [], " "));
1884
+ lines.push(...headerSetLines(resp.headers ?? [], " ", framework));
1780
1885
  if (resp.bodies.length > 0) {
1781
- lines.push(` ctx.type = result.contentType;`);
1782
- lines.push(` ctx.body = ${responseBodyExpr("result.body", bodySchemas.get(resp.statusCode))};`);
1886
+ lines.push(` ${framework.response.type("result.contentType")}`);
1887
+ lines.push(...indent(framework.response.send(responseBodyExpr("result.body", bodySchemas.get(resp.statusCode))), " "));
1888
+ } else {
1889
+ lines.push(...indent(framework.response.send(void 0), " "));
1783
1890
  }
1784
- lines.push(` break;`);
1891
+ lines.push(...indent(framework.response.caseEnd(), " "));
1785
1892
  }
1786
1893
  lines.push(` }`);
1787
1894
  return lines;
@@ -1848,13 +1955,18 @@ function renderHeadersAnnotation(headers, modelsWithOutput) {
1848
1955
  return `{ ${fields.join("; ")} }`;
1849
1956
  }
1850
1957
  __name(renderHeadersAnnotation, "renderHeadersAnnotation");
1851
- function headerSetLines(headers, indent) {
1958
+ function headerSetLines(headers, pad, framework) {
1852
1959
  return headers.map((h) => {
1853
1960
  const accessor = `result.headers[${JSON.stringify(headerNameToProperty(h.name))}]`;
1854
- return h.optional ? `${indent}if (${accessor} !== undefined) ctx.set('${h.name}', String(${accessor}));` : `${indent}ctx.set('${h.name}', String(${accessor}));`;
1961
+ const write = framework.response.header(h.name, `String(${accessor})`);
1962
+ return h.optional ? `${pad}if (${accessor} !== undefined) ${write}` : `${pad}${write}`;
1855
1963
  });
1856
1964
  }
1857
1965
  __name(headerSetLines, "headerSetLines");
1966
+ function indent(lines, pad) {
1967
+ return lines.map((line) => `${pad}${line}`);
1968
+ }
1969
+ __name(indent, "indent");
1858
1970
  function inferService(op, route, file) {
1859
1971
  if (op.service) {
1860
1972
  const [cls = "", method] = op.service.split(".");
@@ -2014,21 +2126,21 @@ function responseBodyExpr(value, schema) {
2014
2126
  return schema ? `await parseAndValidate(${value}, ${schema}, 500)` : value;
2015
2127
  }
2016
2128
  __name(responseBodyExpr, "responseBodyExpr");
2017
- function generateParamValidation(source, ctxExpr, varName, mode, suffix = "", modelsWithInput) {
2129
+ function generateParamValidation(source, kind, sourceExpr, mode, suffix = "", modelsWithInput) {
2018
2130
  if (!source) return [];
2019
2131
  const lines = [];
2020
- const isQuery = ctxExpr === "ctx.query";
2132
+ const isQuery = kind === "query";
2133
+ const isPathParams = kind === "params";
2021
2134
  if (source.kind === "ref") {
2022
2135
  const typeName = modelsWithInput?.has(source.name) ? `${source.name}Input` : source.name;
2023
- lines.push(` const ${varName} = await parseAndValidate(${ctxExpr}, ${typeName}.${mode}());`);
2136
+ lines.push(` const ${kind} = await parseAndValidate(${sourceExpr}, ${typeName}.${mode}());`);
2024
2137
  lines.push("");
2025
2138
  } else if (source.kind === "params") {
2026
2139
  if (source.nodes.length > 0) {
2027
- const isPathParams = ctxExpr === "ctx.params";
2028
2140
  const bind = /* @__PURE__ */ __name((name) => isPathParams ? toIdentifier(name) : name, "bind");
2029
- const lhs = varName === "params" ? `{ ${source.nodes.map((p) => bind(p.name)).join(", ")} }` : varName;
2141
+ const lhs = isPathParams ? `{ ${source.nodes.map((p) => bind(p.name)).join(", ")} }` : kind;
2030
2142
  lines.push(` const ${lhs} = await parseAndValidate(`);
2031
- lines.push(` ${ctxExpr},`);
2143
+ lines.push(` ${sourceExpr},`);
2032
2144
  lines.push(` ${modeToWrapper(mode)}({`);
2033
2145
  for (const param of source.nodes) {
2034
2146
  const bound = bind(param.name);
@@ -2042,7 +2154,7 @@ function generateParamValidation(source, ctxExpr, varName, mode, suffix = "", mo
2042
2154
  }
2043
2155
  } else {
2044
2156
  const schema = isQuery ? renderQueryType(source.node, modelsWithInput) : renderInputType(source.node, modelsWithInput);
2045
- lines.push(` const ${varName} = await parseAndValidate(${ctxExpr}, (${schema}).${mode}());`);
2157
+ lines.push(` const ${kind} = await parseAndValidate(${sourceExpr}, (${schema}).${mode}());`);
2046
2158
  lines.push("");
2047
2159
  }
2048
2160
  return lines;
@@ -2896,7 +3008,7 @@ function sdkResponseMembers(resp, modelsWithOutput, includeStatus) {
2896
3008
  ].join("; ")} }`);
2897
3009
  }
2898
3010
  __name(sdkResponseMembers, "sdkResponseMembers");
2899
- function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive, where) {
3011
+ function sdkReturnLines(resp, modelsWithOutput, indent2, includeStatus, revive, where) {
2900
3012
  const bodies = resp.bodies;
2901
3013
  const headers = resp.headers ?? [];
2902
3014
  const leading = includeStatus ? [
@@ -2907,7 +3019,7 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive, w
2907
3019
  ] : [];
2908
3020
  if (bodies.length === 0) {
2909
3021
  return [
2910
- `${indent}return { ${[
3022
+ `${indent2}return { ${[
2911
3023
  ...leading,
2912
3024
  ...trailing
2913
3025
  ].join(", ")} };`
@@ -2921,7 +3033,7 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive, w
2921
3033
  ...trailing
2922
3034
  ];
2923
3035
  return [
2924
- `${indent}return { ${fields.join(", ")} };`
3036
+ `${indent2}return { ${fields.join(", ")} };`
2925
3037
  ];
2926
3038
  }
2927
3039
  const dataTypes = bodies.map((b) => sdkDataType(b, modelsWithOutput));
@@ -2934,11 +3046,11 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive, w
2934
3046
  ...trailing
2935
3047
  ];
2936
3048
  return [
2937
- `${indent}return { ${fields.join(", ")} };`
3049
+ `${indent2}return { ${fields.join(", ")} };`
2938
3050
  ];
2939
3051
  }
2940
3052
  const lines = [
2941
- `${indent}switch (readContentType(result)) {`
3053
+ `${indent2}switch (readContentType(result)) {`
2942
3054
  ];
2943
3055
  for (const [i, body] of bodies.slice(1).entries()) {
2944
3056
  const fields = [
@@ -2947,8 +3059,8 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive, w
2947
3059
  `data: ${sdkReadExpr(body, modelsWithOutput, hint(revive, `${resp.statusCode}_${i + 1}`))}`,
2948
3060
  ...trailing
2949
3061
  ];
2950
- lines.push(`${indent} case '${body.contentType}':`);
2951
- lines.push(`${indent} return { ${fields.join(", ")} };`);
3062
+ lines.push(`${indent2} case '${body.contentType}':`);
3063
+ lines.push(`${indent2} return { ${fields.join(", ")} };`);
2952
3064
  }
2953
3065
  const first = bodies[0];
2954
3066
  const fallbackFields = [
@@ -2957,9 +3069,9 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive, w
2957
3069
  `data: ${sdkReadExpr(first, modelsWithOutput, hint(revive, `${resp.statusCode}_0`))}`,
2958
3070
  ...trailing
2959
3071
  ];
2960
- lines.push(`${indent} default:`);
2961
- lines.push(`${indent} return { ${fallbackFields.join(", ")} };`);
2962
- lines.push(`${indent}}`);
3072
+ lines.push(`${indent2} default:`);
3073
+ lines.push(`${indent2} return { ${fallbackFields.join(", ")} };`);
3074
+ lines.push(`${indent2}}`);
2963
3075
  return lines;
2964
3076
  }
2965
3077
  __name(sdkReturnLines, "sdkReturnLines");
@@ -4103,6 +4215,24 @@ function renderOutputField(field, outputCase, modelsWithOutput, target) {
4103
4215
  }
4104
4216
  __name(renderOutputField, "renderOutputField");
4105
4217
 
4218
+ // src/server-framework.ts
4219
+ var SERVER_FRAMEWORK_NAMES = [
4220
+ "koa"
4221
+ ];
4222
+ var DEFAULT_SERVER_FRAMEWORK_NAME = "koa";
4223
+ var SERVER_FRAMEWORKS = {
4224
+ koa: KOA_SERVER_FRAMEWORK
4225
+ };
4226
+ function resolveServerFramework(name) {
4227
+ const resolved = name ?? DEFAULT_SERVER_FRAMEWORK_NAME;
4228
+ const framework = SERVER_FRAMEWORKS[resolved];
4229
+ if (!framework) {
4230
+ throw new Error(`plugin-typescript: server.framework '${resolved}' is not supported \u2014 expected one of: ${SERVER_FRAMEWORK_NAMES.join(", ")}.`);
4231
+ }
4232
+ return framework;
4233
+ }
4234
+ __name(resolveServerFramework, "resolveServerFramework");
4235
+
4106
4236
  // src/codegen-mcp.ts
4107
4237
  import { resolveModifiers as resolveModifiers3, emittedResponses as emittedResponses2, toIdentifier as toIdentifier3 } from "@contractkit/core";
4108
4238
  import { basename as basename3, dirname as dirname5, relative as relative5 } from "path";
@@ -4415,7 +4545,8 @@ function renderToolClass(plan, file, options) {
4415
4545
  const callArgs = buildArgs(route, op);
4416
4546
  const isVoid = !primaryResponseBody(op);
4417
4547
  const structured = !!outExpr;
4418
- lines.push(" async handle(args: Record<string, unknown>, _context: McpToolContext): Promise<CallToolResult> {");
4548
+ const argsParam = destructure.length > 0 ? "args" : "_args";
4549
+ lines.push(` async handle(${argsParam}: Record<string, unknown>, _context: McpToolContext): Promise<CallToolResult> {`);
4419
4550
  if (destructure.length > 0) {
4420
4551
  lines.push(` const { ${destructure.join(", ")} } = await parseAndValidate(args, ${argsConstName});`);
4421
4552
  }
@@ -4503,40 +4634,29 @@ function generateMcpAggregator(entries) {
4503
4634
  lines.push(`import { McpToolHandlerMap } from '@maroonedsoftware/mcp';`);
4504
4635
  for (const e of sorted) lines.push(`import { ${e.registerFn} } from '${e.importPath}';`);
4505
4636
  lines.push("");
4506
- lines.push("/** Build + register the MCP tool catalog. Call once at startup. */");
4637
+ lines.push("/**");
4638
+ lines.push(" * Build the MCP tool catalog.");
4639
+ lines.push(" *");
4640
+ lines.push(" * Bind it to the `McpToolHandlerMap` token from a factory, which is what supplies the");
4641
+ lines.push(" * `Container` needed to resolve each handler:");
4642
+ lines.push(" *");
4643
+ lines.push(" * ```ts");
4644
+ lines.push(" * registry.register(McpToolHandlerMap).useFactory(registerMcpTools).asSingleton();");
4645
+ lines.push(" * ```");
4646
+ lines.push(" */");
4507
4647
  lines.push("export function registerMcpTools(container: Container): McpToolHandlerMap {");
4508
4648
  lines.push(" const map = new McpToolHandlerMap();");
4509
4649
  for (const e of sorted) lines.push(` ${e.registerFn}(map, container);`);
4510
- lines.push(" container.register(McpToolHandlerMap, { useValue: map });");
4511
4650
  lines.push(" return map;");
4512
4651
  lines.push("}");
4513
4652
  return lines.join("\n") + "\n";
4514
4653
  }
4515
4654
  __name(generateMcpAggregator, "generateMcpAggregator");
4516
4655
  function generateMcpRouter(options = {}) {
4517
- const path = options.path ?? "/mcp";
4518
- return `import { ServerKitRouter, bodyParserMiddleware, requireSignature } from '@maroonedsoftware/koa';
4519
- import { McpDispatcher, createMcpRequestContext, MCP_AUTH_POLICY } from '@maroonedsoftware/mcp';
4520
-
4521
- /** Mount the MCP endpoint onto a ServerKit router. Call \`registerMcpTools(container)\` at startup. */
4522
- export function mountMcp(router: ReturnType<typeof ServerKitRouter>): void {
4523
- router.post('${path}', bodyParserMiddleware(['json']), requireSignature('mcp', { policy: MCP_AUTH_POLICY }), async (ctx) => {
4524
- const dispatcher = ctx.container.get(McpDispatcher);
4525
- const context = createMcpRequestContext({ requestId: ctx.requestId, logger: ctx.logger });
4526
- if (dispatcher.sessionMode === 'stateful') {
4527
- ctx.respond = false;
4528
- await dispatcher.dispatchStateful(
4529
- { req: ctx.req, res: ctx.res, body: ctx.parsedBody, sessionId: ctx.get('mcp-session-id') },
4530
- context,
4531
- );
4532
- } else {
4533
- const response = await dispatcher.dispatch(JSON.parse(String(ctx.rawBody)), context);
4534
- if (response) ctx.body = response;
4535
- else ctx.status = 202; // a notification \u2014 nothing to return
4536
- }
4537
- });
4538
- }
4539
- `;
4656
+ const framework = options.framework ?? KOA_SERVER_FRAMEWORK;
4657
+ return framework.mcpRouter({
4658
+ path: options.path ?? "/mcp"
4659
+ });
4540
4660
  }
4541
4661
  __name(generateMcpRouter, "generateMcpRouter");
4542
4662
 
@@ -4770,6 +4890,10 @@ function createTypescriptPlugin(config, rootDir) {
4770
4890
  }
4771
4891
  __name(createTypescriptPlugin, "createTypescriptPlugin");
4772
4892
  function assertValidConfig(config) {
4893
+ const framework = config.server?.framework;
4894
+ if (framework !== void 0 && !SERVER_FRAMEWORK_NAMES.includes(framework)) {
4895
+ throw new Error(`plugin-typescript: server.framework '${String(framework)}' is not supported \u2014 expected one of: ${SERVER_FRAMEWORK_NAMES.join(", ")}.`);
4896
+ }
4773
4897
  if (config.server?.validateResponses && !config.server.zod) {
4774
4898
  throw new Error("plugin-typescript: server.validateResponses requires server.zod: true \u2014 without it output.types emits plain TypeScript interfaces, which are types with no runtime schema value for the router to validate against.");
4775
4899
  }
@@ -4899,6 +5023,7 @@ function sliceModelSet(refs, ownNames, set) {
4899
5023
  __name(sliceModelSet, "sliceModelSet");
4900
5024
  function collectServerOutput(config, rootDir, inputs, units) {
4901
5025
  const serverBase = resolve2(rootDir, config.baseDir ?? ".");
5026
+ const framework = resolveServerFramework(config.framework);
4902
5027
  const modelsWithInput = inputs.modelsWithInput;
4903
5028
  const modelsWithOutput = inputs.modelsWithOutput;
4904
5029
  const modelsWithTransform = computeModelsWithCaseTransform(inputs.contractRoots.flatMap((r) => r.models));
@@ -4947,7 +5072,7 @@ function collectServerOutput(config, rootDir, inputs, units) {
4947
5072
  currentOutPath: typeOutPath,
4948
5073
  modelsWithInput,
4949
5074
  modelsWithOutput,
4950
- // These types are consumed by Koa handlers, so `binary` is a Buffer, not a Blob.
5075
+ // These types are consumed by server handlers, so `binary` is a Buffer, not a Blob.
4951
5076
  target: "server"
4952
5077
  };
4953
5078
  const content = config.zod ? generateContract(ast, renderCtx) : generatePlainTypes(ast, renderCtx);
@@ -4978,6 +5103,9 @@ function collectServerOutput(config, rootDir, inputs, units) {
4978
5103
  // this router's output with no change to `root` or the config.
4979
5104
  modelsWithTransform: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithTransform),
4980
5105
  validateResponses: config.validateResponses ?? false,
5106
+ // Covered by `sub` already, which is the whole sub-config; explicit for the same reason
5107
+ // `validateResponses` is — the inputs that change a router's text read at a glance.
5108
+ framework: framework.name,
4981
5109
  sub: subConfigKey
4982
5110
  });
4983
5111
  units.push({
@@ -4994,7 +5122,8 @@ function collectServerOutput(config, rootDir, inputs, units) {
4994
5122
  modelsWithOutput,
4995
5123
  modelsWithTransform,
4996
5124
  includeInternal: config.includeInternal,
4997
- validateResponses: config.validateResponses
5125
+ validateResponses: config.validateResponses,
5126
+ framework
4998
5127
  })
4999
5128
  }
5000
5129
  ], "render")
@@ -5616,10 +5745,12 @@ function collectMcpOutput(config, fullConfig, rootDir, inputs, units, globalFile
5616
5745
  });
5617
5746
  if (config.emitRouter !== false) {
5618
5747
  const routerPath = join2(mcpBase, config.output?.router ?? "mcp.router.ts");
5748
+ const framework = resolveServerFramework(fullConfig.server?.framework);
5619
5749
  globalFiles.push({
5620
5750
  relativePath: routerPath,
5621
5751
  content: generateMcpRouter({
5622
- path: config.path
5752
+ path: config.path,
5753
+ framework
5623
5754
  })
5624
5755
  });
5625
5756
  }
@@ -5677,8 +5808,13 @@ function stableSubConfig(config) {
5677
5808
  }
5678
5809
  __name(stableSubConfig, "stableSubConfig");
5679
5810
  export {
5811
+ DEFAULT_SERVER_FRAMEWORK_NAME,
5812
+ KOA_SERVER_FRAMEWORK,
5813
+ SERVER_FRAMEWORKS,
5814
+ SERVER_FRAMEWORK_NAMES,
5680
5815
  TYPESCRIPT_CODEGEN_VERSION,
5681
5816
  createTypescriptPlugin,
5682
- index_default as default
5817
+ index_default as default,
5818
+ resolveServerFramework
5683
5819
  };
5684
5820
  //# sourceMappingURL=index.js.map