@contractkit/plugin-typescript 0.34.1 → 0.36.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,138 @@ __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
+ handlerLocals: [
1480
+ "ctx"
1481
+ ],
1482
+ routeOpen(routerName, method, path, middlewares) {
1483
+ const middlewareStr = middlewares.length > 0 ? `, ${middlewares.join(", ")},` : ",";
1484
+ return `${routerName}.${method}('${path}'${middlewareStr} async ctx => {`;
1485
+ },
1486
+ routeClose() {
1487
+ return [
1488
+ "});"
1489
+ ];
1490
+ },
1491
+ middleware: {
1492
+ policy(args) {
1493
+ return `requirePolicy(${args})`;
1494
+ },
1495
+ bodyParser(tokensExpr) {
1496
+ return `bodyParserMiddleware([${tokensExpr}])`;
1497
+ },
1498
+ signature(args) {
1499
+ return `requireSignature(${args})`;
1500
+ }
1501
+ },
1502
+ request: {
1503
+ params: "ctx.params",
1504
+ query: "ctx.query",
1505
+ headers: "ctx.headers",
1506
+ // Not `ctx.request.body`: the ServerKit body parser drains the stream and writes its result
1507
+ // here, and in Koa `ctx.body` is the *response* body.
1508
+ parsedBody: "ctx.parsedBody",
1509
+ // Koa strips the parameters off `Content-Type` for this accessor already.
1510
+ contentType: "ctx.request.type"
1511
+ },
1512
+ resolveService(className) {
1513
+ return `ctx.container.get(${className})`;
1514
+ },
1515
+ response: {
1516
+ status(expr) {
1517
+ return `ctx.status = ${expr};`;
1518
+ },
1519
+ header(name, valueExpr) {
1520
+ return `ctx.set('${name}', ${valueExpr});`;
1521
+ },
1522
+ type(expr) {
1523
+ return `ctx.type = ${expr};`;
1524
+ },
1525
+ send(bodyExpr) {
1526
+ return bodyExpr === void 0 ? [] : [
1527
+ `ctx.body = ${bodyExpr};`
1528
+ ];
1529
+ },
1530
+ caseEnd() {
1531
+ return [
1532
+ "break;"
1533
+ ];
1534
+ }
1535
+ },
1536
+ mcpRouter({ path }) {
1537
+ return `import { ServerKitRouter, bodyParserMiddleware, requireSignature } from '${KOA_RUNTIME_MODULE}';
1538
+ import { McpDispatcher, createMcpRequestContext, MCP_AUTH_POLICY } from '@maroonedsoftware/mcp';
1539
+
1540
+ /** Mount the MCP endpoint onto a ServerKit router. Bind \`registerMcpTools\` to the \`McpToolHandlerMap\` token. */
1541
+ export function mountMcp(router: ReturnType<typeof ServerKitRouter>): void {
1542
+ router.post('${path}', bodyParserMiddleware(['json']), requireSignature('mcp', { policy: MCP_AUTH_POLICY }), async (ctx) => {
1543
+ const dispatcher = ctx.container.get(McpDispatcher);
1544
+ const context = createMcpRequestContext({ requestId: ctx.requestId, logger: ctx.logger });
1545
+ if (dispatcher.sessionMode === 'stateful') {
1546
+ ctx.respond = false;
1547
+ await dispatcher.dispatchStateful(
1548
+ { req: ctx.req, res: ctx.res, body: ctx.parsedBody, sessionId: ctx.get('mcp-session-id') },
1549
+ context,
1550
+ );
1551
+ } else {
1552
+ const response = await dispatcher.dispatch(JSON.parse(String(ctx.rawBody)), context);
1553
+ if (response) ctx.body = response;
1554
+ else ctx.status = 202; // a notification \u2014 nothing to return
1555
+ }
1556
+ });
1557
+ }
1558
+ `;
1559
+ }
1560
+ };
1561
+
1562
+ // src/codegen-operation.ts
1563
+ var GENERATOR_HANDLER_LOCALS = [
1564
+ "service",
1565
+ "result",
1566
+ "body",
1567
+ "multipartBody",
1568
+ "params",
1569
+ "query",
1570
+ "headers"
1571
+ ];
1572
+ function bindPathParams(nodes, handlerLocals) {
1573
+ const reserved = /* @__PURE__ */ new Set([
1574
+ ...handlerLocals,
1575
+ ...GENERATOR_HANDLER_LOCALS
1576
+ ]);
1577
+ const taken = /* @__PURE__ */ new Set();
1578
+ const bindings = /* @__PURE__ */ new Map();
1579
+ for (const node of nodes) {
1580
+ let local = toIdentifier(node.name);
1581
+ while (reserved.has(local) || taken.has(local)) local += "_";
1582
+ taken.add(local);
1583
+ bindings.set(node.name, local);
1584
+ }
1585
+ return bindings;
1586
+ }
1587
+ __name(bindPathParams, "bindPathParams");
1456
1588
  function bodyParserToken(contentType) {
1457
1589
  switch (classifyContentType(contentType)) {
1458
1590
  case "urlencoded":
@@ -1525,6 +1657,11 @@ function bodyTypesStructurallyEqual(a, b) {
1525
1657
  }
1526
1658
  __name(bodyTypesStructurallyEqual, "bodyTypesStructurallyEqual");
1527
1659
  function generateOp(root, options = {}) {
1660
+ const resolved = {
1661
+ ...options,
1662
+ framework: options.framework ?? KOA_SERVER_FRAMEWORK
1663
+ };
1664
+ const framework = resolved.framework;
1528
1665
  const types = collectTypes(root, options.modelsWithInput, options.modelsWithOutput);
1529
1666
  const services = collectServices(root);
1530
1667
  const routerName = deriveRouterName(root.file);
@@ -1533,13 +1670,13 @@ function generateOp(root, options = {}) {
1533
1670
  lines.push("/**");
1534
1671
  lines.push(` * generated from ${sourceLink(basename(root.file), options.outPath, root.file)}`);
1535
1672
  lines.push("*/");
1536
- lines.push(`export const ${routerName} = ServerKitRouter();`);
1673
+ lines.push(framework.routerDeclaration(routerName));
1537
1674
  lines.push("");
1538
1675
  const includeInternal = options.includeInternal ?? true;
1539
1676
  for (const route of root.routes) {
1540
1677
  for (const op of route.operations) {
1541
1678
  if (!includeInternal && resolveModifiers(route, op).includes("internal")) continue;
1542
- lines.push(...generateHandler(route, op, root, options));
1679
+ lines.push(...generateHandler(route, op, root, resolved));
1543
1680
  lines.push("");
1544
1681
  }
1545
1682
  }
@@ -1574,15 +1711,7 @@ function generateOp(root, options = {}) {
1574
1711
  ].join("\n");
1575
1712
  const uses = /* @__PURE__ */ __name((symbol) => new RegExp(`\\b${symbol}\\b`).test(generated), "uses");
1576
1713
  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
- }
1714
+ body.push(...framework.imports(uses));
1586
1715
  for (const svc of services.filter(uses)) {
1587
1716
  const modulePath = root.services?.[svc] ?? root.meta[svc] ?? deriveModulePath(svc, options.servicePathTemplate);
1588
1717
  body.push(`import { ${svc} } from '${modulePath}';`);
@@ -1626,6 +1755,7 @@ function generateHandler(route, op, root, options) {
1626
1755
  const file = root.file;
1627
1756
  const outPath = options.outPath;
1628
1757
  const modelsWithInput = options.modelsWithInput;
1758
+ const framework = options.framework;
1629
1759
  lines.push("/**");
1630
1760
  const desc = op.description ?? route.description;
1631
1761
  if (desc) {
@@ -1641,7 +1771,7 @@ function generateHandler(route, op, root, options) {
1641
1771
  if (mods.includes("deprecated")) lines.push(` * @deprecated`);
1642
1772
  lines.push("*/");
1643
1773
  const method = op.method;
1644
- const path = route.path.replace(PATH_PARAM_RE_G, (_m, name) => `:${toIdentifier(name)}`);
1774
+ const path = route.path.replace(PATH_PARAM_RE_G, (_m, name) => framework.pathParam(toIdentifier(name)));
1645
1775
  const bodies = op.request?.bodies ?? [];
1646
1776
  const hasBody = bodies.length > 0;
1647
1777
  const isSingleMultipart = bodies.length === 1 && bodies[0].contentType === "multipart/form-data";
@@ -1649,42 +1779,42 @@ function generateHandler(route, op, root, options) {
1649
1779
  if (effectiveSecurity !== SECURITY_NONE) {
1650
1780
  const policy = effectiveSecurity?.policy;
1651
1781
  const args = policy === void 0 ? "" : policy === false ? "{ policy: false }" : `{ policy: '${policy}' }`;
1652
- middlewares.push(`requirePolicy(${args})`);
1782
+ middlewares.push(framework.middleware.policy(args));
1653
1783
  }
1654
1784
  if (hasBody) {
1655
1785
  const parserTokens = Array.from(new Set(bodies.map((b) => bodyParserToken(b.contentType))));
1656
1786
  const tokensExpr = parserTokens.map((t) => `'${t}'`).join(", ");
1657
- middlewares.push(`bodyParserMiddleware([${tokensExpr}])`);
1787
+ middlewares.push(framework.middleware.bodyParser(tokensExpr));
1658
1788
  }
1659
1789
  if (op.signature) {
1660
1790
  const sigArgs = op.signaturePolicy ? `'${escapeSingleQuoted(op.signature)}', { policy: '${escapeSingleQuoted(op.signaturePolicy)}' }` : `'${escapeSingleQuoted(op.signature)}'`;
1661
- middlewares.push(`requireSignature(${sigArgs})`);
1791
+ middlewares.push(framework.middleware.signature(sigArgs));
1662
1792
  }
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));
1793
+ lines.push(framework.routeOpen(deriveRouterName(file), method, path, middlewares));
1794
+ const pathBindings = route.params?.kind === "params" ? bindPathParams(route.params.nodes, framework.handlerLocals) : void 0;
1795
+ lines.push(...generateParamValidation(route.params, "params", framework.request.params, route.paramsMode ?? "strict", "", modelsWithInput, pathBindings));
1796
+ lines.push(...generateParamValidation(op.query, "query", framework.request.query, op.queryMode ?? "strict", "", modelsWithInput));
1797
+ lines.push(...generateParamValidation(op.headers, "headers", framework.request.headers, op.headersMode ?? "strip", "", modelsWithInput));
1668
1798
  if (hasBody && op.request) {
1669
1799
  if (isSingleMultipart) {
1670
- lines.push(` const multipartBody = ctx.parsedBody as MultipartBody;`);
1800
+ lines.push(` const multipartBody = ${framework.request.parsedBody} as MultipartBody;`);
1671
1801
  lines.push("");
1672
1802
  } else if (bodies.length === 1) {
1673
- lines.push(` const body = await parseAndValidate(ctx.parsedBody, ${renderInputType(bodies[0].bodyType, modelsWithInput)});`);
1803
+ lines.push(` const body = await parseAndValidate(${framework.request.parsedBody}, ${renderInputType(bodies[0].bodyType, modelsWithInput)});`);
1674
1804
  lines.push("");
1675
1805
  } 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)});`);
1806
+ lines.push(` const body = await parseAndValidate(${framework.request.parsedBody}, ${renderInputType(bodies[0].bodyType, modelsWithInput)});`);
1677
1807
  lines.push("");
1678
1808
  } else {
1679
1809
  const annotation = bodies.map((b) => b.contentType === "multipart/form-data" ? "MultipartBody" : `z.infer<typeof ${renderInputType(b.bodyType, modelsWithInput)}>`).join(" | ");
1680
1810
  lines.push(` let body!: ${annotation};`);
1681
- lines.push(` switch (ctx.request.type) {`);
1811
+ lines.push(` switch (${framework.request.contentType}) {`);
1682
1812
  for (const b of bodies) {
1683
1813
  lines.push(` case '${b.contentType}':`);
1684
1814
  if (b.contentType === "multipart/form-data") {
1685
- lines.push(` body = ctx.parsedBody as MultipartBody;`);
1815
+ lines.push(` body = ${framework.request.parsedBody} as MultipartBody;`);
1686
1816
  } else {
1687
- lines.push(` body = await parseAndValidate(ctx.parsedBody, ${renderInputType(b.bodyType, modelsWithInput)});`);
1817
+ lines.push(` body = await parseAndValidate(${framework.request.parsedBody}, ${renderInputType(b.bodyType, modelsWithInput)});`);
1688
1818
  }
1689
1819
  lines.push(` break;`);
1690
1820
  }
@@ -1694,17 +1824,18 @@ function generateHandler(route, op, root, options) {
1694
1824
  }
1695
1825
  const emitted = emittedResponses(op);
1696
1826
  const serviceParts = inferService(op, route, file);
1697
- const call = `await service.${serviceParts.methodName}(${buildArgs(route, op)})`;
1827
+ const call = `await service.${serviceParts.methodName}(${buildArgs(route, op, pathBindings)})`;
1698
1828
  if (emitted.length > 1) {
1699
1829
  lines.push(...generateMultiStatusResult(emitted, serviceParts.className, call, options));
1700
1830
  } else {
1701
1831
  lines.push(...generateSingleStatusResult(emitted[0], op, serviceParts.className, call, options));
1702
1832
  }
1703
- lines.push(`});`);
1833
+ lines.push(...framework.routeClose());
1704
1834
  return lines;
1705
1835
  }
1706
1836
  __name(generateHandler, "generateHandler");
1707
1837
  function generateSingleStatusResult(resp, op, className, call, options) {
1838
+ const framework = options.framework;
1708
1839
  const lines = [];
1709
1840
  const bodies = resp ? resp.bodies : [];
1710
1841
  const respHeaders = resp?.headers ?? [];
@@ -1715,7 +1846,7 @@ function generateSingleStatusResult(resp, op, className, call, options) {
1715
1846
  const { annotation, prelude } = formatTypeAnnotation(bodies[0].bodyType, options.modelsWithOutput);
1716
1847
  if (prelude) lines.push(` ${prelude}`);
1717
1848
  bodySchema = responseBodySchema(bodies[0].bodyType, options, prelude ? "resultType" : void 0);
1718
- lines.push(` const service = ctx.container.get(${className});`);
1849
+ lines.push(` const service = ${framework.resolveService(className)};`);
1719
1850
  if (hasRespHeaders) {
1720
1851
  lines.push(` const result: { body: ${annotation}; headers: ${headersAnnotation} } = ${call};`);
1721
1852
  } else {
@@ -1729,10 +1860,10 @@ function generateSingleStatusResult(resp, op, className, call, options) {
1729
1860
  const { members, preludes } = rendered;
1730
1861
  bodySchema = rendered.bodySchema;
1731
1862
  for (const prelude of preludes) lines.push(` ${prelude}`);
1732
- lines.push(` const service = ctx.container.get(${className});`);
1863
+ lines.push(` const service = ${framework.resolveService(className)};`);
1733
1864
  lines.push(` const result: ${members.join(" | ")} = ${call};`);
1734
1865
  } else {
1735
- lines.push(` const service = ctx.container.get(${className});`);
1866
+ lines.push(` const service = ${framework.resolveService(className)};`);
1736
1867
  if (hasRespHeaders) {
1737
1868
  lines.push(` const result: { headers: ${headersAnnotation} } = ${call};`);
1738
1869
  } else {
@@ -1740,19 +1871,22 @@ function generateSingleStatusResult(resp, op, className, call, options) {
1740
1871
  }
1741
1872
  }
1742
1873
  lines.push("");
1743
- lines.push(` ctx.status = ${resp?.statusCode ?? 204};`);
1744
- lines.push(...headerSetLines(respHeaders, " "));
1874
+ lines.push(` ${framework.response.status(String(resp?.statusCode ?? 204))}`);
1875
+ lines.push(...headerSetLines(respHeaders, " ", framework));
1745
1876
  if (bodies.length === 1) {
1746
- lines.push(` ctx.type = '${bodies[0].contentType}';`);
1747
- lines.push(` ctx.body = ${responseBodyExpr(hasRespHeaders ? "result.body" : "result", bodySchema)};`);
1877
+ lines.push(` ${framework.response.type(`'${bodies[0].contentType}'`)}`);
1878
+ lines.push(...indent(framework.response.send(responseBodyExpr(hasRespHeaders ? "result.body" : "result", bodySchema)), " "));
1748
1879
  } else if (bodies.length > 1) {
1749
- lines.push(` ctx.type = result.contentType;`);
1750
- lines.push(` ctx.body = ${responseBodyExpr("result.body", bodySchema)};`);
1880
+ lines.push(` ${framework.response.type("result.contentType")}`);
1881
+ lines.push(...indent(framework.response.send(responseBodyExpr("result.body", bodySchema)), " "));
1882
+ } else {
1883
+ lines.push(...indent(framework.response.send(void 0), " "));
1751
1884
  }
1752
1885
  return lines;
1753
1886
  }
1754
1887
  __name(generateSingleStatusResult, "generateSingleStatusResult");
1755
1888
  function generateMultiStatusResult(emitted, className, call, options) {
1889
+ const framework = options.framework;
1756
1890
  const lines = [];
1757
1891
  const members = [];
1758
1892
  const preludes = [];
@@ -1767,21 +1901,23 @@ function generateMultiStatusResult(emitted, className, call, options) {
1767
1901
  bodySchemas.set(resp.statusCode, rendered.bodySchema);
1768
1902
  }
1769
1903
  for (const prelude of preludes) lines.push(` ${prelude}`);
1770
- lines.push(` const service = ctx.container.get(${className});`);
1904
+ lines.push(` const service = ${framework.resolveService(className)};`);
1771
1905
  lines.push(` const result:`);
1772
1906
  for (const member of members) lines.push(` | ${member}`);
1773
1907
  lines.push(` = ${call};`);
1774
1908
  lines.push("");
1775
- lines.push(` ctx.status = result.status;`);
1909
+ lines.push(` ${framework.response.status("result.status")}`);
1776
1910
  lines.push(` switch (result.status) {`);
1777
1911
  for (const resp of emitted) {
1778
1912
  lines.push(` case ${resp.statusCode}:`);
1779
- lines.push(...headerSetLines(resp.headers ?? [], " "));
1913
+ lines.push(...headerSetLines(resp.headers ?? [], " ", framework));
1780
1914
  if (resp.bodies.length > 0) {
1781
- lines.push(` ctx.type = result.contentType;`);
1782
- lines.push(` ctx.body = ${responseBodyExpr("result.body", bodySchemas.get(resp.statusCode))};`);
1915
+ lines.push(` ${framework.response.type("result.contentType")}`);
1916
+ lines.push(...indent(framework.response.send(responseBodyExpr("result.body", bodySchemas.get(resp.statusCode))), " "));
1917
+ } else {
1918
+ lines.push(...indent(framework.response.send(void 0), " "));
1783
1919
  }
1784
- lines.push(` break;`);
1920
+ lines.push(...indent(framework.response.caseEnd(), " "));
1785
1921
  }
1786
1922
  lines.push(` }`);
1787
1923
  return lines;
@@ -1848,13 +1984,18 @@ function renderHeadersAnnotation(headers, modelsWithOutput) {
1848
1984
  return `{ ${fields.join("; ")} }`;
1849
1985
  }
1850
1986
  __name(renderHeadersAnnotation, "renderHeadersAnnotation");
1851
- function headerSetLines(headers, indent) {
1987
+ function headerSetLines(headers, pad, framework) {
1852
1988
  return headers.map((h) => {
1853
1989
  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}));`;
1990
+ const write = framework.response.header(h.name, `String(${accessor})`);
1991
+ return h.optional ? `${pad}if (${accessor} !== undefined) ${write}` : `${pad}${write}`;
1855
1992
  });
1856
1993
  }
1857
1994
  __name(headerSetLines, "headerSetLines");
1995
+ function indent(lines, pad) {
1996
+ return lines.map((line) => `${pad}${line}`);
1997
+ }
1998
+ __name(indent, "indent");
1858
1999
  function inferService(op, route, file) {
1859
2000
  if (op.service) {
1860
2001
  const [cls = "", method] = op.service.split(".");
@@ -1890,11 +2031,11 @@ function inferMethodName(method, path) {
1890
2031
  }
1891
2032
  }
1892
2033
  __name(inferMethodName, "inferMethodName");
1893
- function buildArgs(route, op) {
2034
+ function buildArgs(route, op, bindings) {
1894
2035
  const args = [];
1895
2036
  if (route.params) {
1896
2037
  if (route.params.kind === "params") {
1897
- args.push(...route.params.nodes.map((p) => toIdentifier(p.name)));
2038
+ args.push(...route.params.nodes.map((p) => bindings?.get(p.name) ?? toIdentifier(p.name)));
1898
2039
  } else {
1899
2040
  args.push("params");
1900
2041
  }
@@ -2014,21 +2155,25 @@ function responseBodyExpr(value, schema) {
2014
2155
  return schema ? `await parseAndValidate(${value}, ${schema}, 500)` : value;
2015
2156
  }
2016
2157
  __name(responseBodyExpr, "responseBodyExpr");
2017
- function generateParamValidation(source, ctxExpr, varName, mode, suffix = "", modelsWithInput) {
2158
+ function generateParamValidation(source, kind, sourceExpr, mode, suffix = "", modelsWithInput, bindings) {
2018
2159
  if (!source) return [];
2019
2160
  const lines = [];
2020
- const isQuery = ctxExpr === "ctx.query";
2161
+ const isQuery = kind === "query";
2162
+ const isPathParams = kind === "params";
2021
2163
  if (source.kind === "ref") {
2022
2164
  const typeName = modelsWithInput?.has(source.name) ? `${source.name}Input` : source.name;
2023
- lines.push(` const ${varName} = await parseAndValidate(${ctxExpr}, ${typeName}.${mode}());`);
2165
+ lines.push(` const ${kind} = await parseAndValidate(${sourceExpr}, ${typeName}.${mode}());`);
2024
2166
  lines.push("");
2025
2167
  } else if (source.kind === "params") {
2026
2168
  if (source.nodes.length > 0) {
2027
- const isPathParams = ctxExpr === "ctx.params";
2028
2169
  const bind = /* @__PURE__ */ __name((name) => isPathParams ? toIdentifier(name) : name, "bind");
2029
- const lhs = varName === "params" ? `{ ${source.nodes.map((p) => bind(p.name)).join(", ")} }` : varName;
2170
+ const lhs = isPathParams ? `{ ${source.nodes.map((p) => {
2171
+ const wire = bind(p.name);
2172
+ const local = bindings?.get(p.name) ?? wire;
2173
+ return wire === local ? wire : `${wire}: ${local}`;
2174
+ }).join(", ")} }` : kind;
2030
2175
  lines.push(` const ${lhs} = await parseAndValidate(`);
2031
- lines.push(` ${ctxExpr},`);
2176
+ lines.push(` ${sourceExpr},`);
2032
2177
  lines.push(` ${modeToWrapper(mode)}({`);
2033
2178
  for (const param of source.nodes) {
2034
2179
  const bound = bind(param.name);
@@ -2042,7 +2187,7 @@ function generateParamValidation(source, ctxExpr, varName, mode, suffix = "", mo
2042
2187
  }
2043
2188
  } else {
2044
2189
  const schema = isQuery ? renderQueryType(source.node, modelsWithInput) : renderInputType(source.node, modelsWithInput);
2045
- lines.push(` const ${varName} = await parseAndValidate(${ctxExpr}, (${schema}).${mode}());`);
2190
+ lines.push(` const ${kind} = await parseAndValidate(${sourceExpr}, (${schema}).${mode}());`);
2046
2191
  lines.push("");
2047
2192
  }
2048
2193
  return lines;
@@ -2896,7 +3041,7 @@ function sdkResponseMembers(resp, modelsWithOutput, includeStatus) {
2896
3041
  ].join("; ")} }`);
2897
3042
  }
2898
3043
  __name(sdkResponseMembers, "sdkResponseMembers");
2899
- function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive, where) {
3044
+ function sdkReturnLines(resp, modelsWithOutput, indent2, includeStatus, revive, where) {
2900
3045
  const bodies = resp.bodies;
2901
3046
  const headers = resp.headers ?? [];
2902
3047
  const leading = includeStatus ? [
@@ -2907,7 +3052,7 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive, w
2907
3052
  ] : [];
2908
3053
  if (bodies.length === 0) {
2909
3054
  return [
2910
- `${indent}return { ${[
3055
+ `${indent2}return { ${[
2911
3056
  ...leading,
2912
3057
  ...trailing
2913
3058
  ].join(", ")} };`
@@ -2921,7 +3066,7 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive, w
2921
3066
  ...trailing
2922
3067
  ];
2923
3068
  return [
2924
- `${indent}return { ${fields.join(", ")} };`
3069
+ `${indent2}return { ${fields.join(", ")} };`
2925
3070
  ];
2926
3071
  }
2927
3072
  const dataTypes = bodies.map((b) => sdkDataType(b, modelsWithOutput));
@@ -2934,11 +3079,11 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive, w
2934
3079
  ...trailing
2935
3080
  ];
2936
3081
  return [
2937
- `${indent}return { ${fields.join(", ")} };`
3082
+ `${indent2}return { ${fields.join(", ")} };`
2938
3083
  ];
2939
3084
  }
2940
3085
  const lines = [
2941
- `${indent}switch (readContentType(result)) {`
3086
+ `${indent2}switch (readContentType(result)) {`
2942
3087
  ];
2943
3088
  for (const [i, body] of bodies.slice(1).entries()) {
2944
3089
  const fields = [
@@ -2947,8 +3092,8 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive, w
2947
3092
  `data: ${sdkReadExpr(body, modelsWithOutput, hint(revive, `${resp.statusCode}_${i + 1}`))}`,
2948
3093
  ...trailing
2949
3094
  ];
2950
- lines.push(`${indent} case '${body.contentType}':`);
2951
- lines.push(`${indent} return { ${fields.join(", ")} };`);
3095
+ lines.push(`${indent2} case '${body.contentType}':`);
3096
+ lines.push(`${indent2} return { ${fields.join(", ")} };`);
2952
3097
  }
2953
3098
  const first = bodies[0];
2954
3099
  const fallbackFields = [
@@ -2957,9 +3102,9 @@ function sdkReturnLines(resp, modelsWithOutput, indent, includeStatus, revive, w
2957
3102
  `data: ${sdkReadExpr(first, modelsWithOutput, hint(revive, `${resp.statusCode}_0`))}`,
2958
3103
  ...trailing
2959
3104
  ];
2960
- lines.push(`${indent} default:`);
2961
- lines.push(`${indent} return { ${fallbackFields.join(", ")} };`);
2962
- lines.push(`${indent}}`);
3105
+ lines.push(`${indent2} default:`);
3106
+ lines.push(`${indent2} return { ${fallbackFields.join(", ")} };`);
3107
+ lines.push(`${indent2}}`);
2963
3108
  return lines;
2964
3109
  }
2965
3110
  __name(sdkReturnLines, "sdkReturnLines");
@@ -4103,6 +4248,142 @@ function renderOutputField(field, outputCase, modelsWithOutput, target) {
4103
4248
  }
4104
4249
  __name(renderOutputField, "renderOutputField");
4105
4250
 
4251
+ // src/server-framework-fastify.ts
4252
+ var FASTIFY_RUNTIME_MODULE = "@maroonedsoftware/fastify";
4253
+ var FASTIFY_RUNTIME_SYMBOLS = [
4254
+ "ServerKitRouter",
4255
+ "bodyParserMiddleware",
4256
+ "requirePolicy",
4257
+ "requireSignature",
4258
+ "requestMediaType"
4259
+ ];
4260
+ var FASTIFY_SERVER_FRAMEWORK = {
4261
+ name: "fastify",
4262
+ imports(uses) {
4263
+ const symbols = FASTIFY_RUNTIME_SYMBOLS.filter(uses);
4264
+ return symbols.length > 0 ? [
4265
+ `import { ${symbols.join(", ")} } from '${FASTIFY_RUNTIME_MODULE}';`
4266
+ ] : [];
4267
+ },
4268
+ routerDeclaration(routerName) {
4269
+ return `export const ${routerName} = ServerKitRouter();`;
4270
+ },
4271
+ pathParam(identifier) {
4272
+ return `:${identifier}`;
4273
+ },
4274
+ handlerLocals: [
4275
+ "request",
4276
+ "reply"
4277
+ ],
4278
+ routeOpen(routerName, method, path, middlewares) {
4279
+ const middlewareStr = middlewares.length > 0 ? `, ${middlewares.join(", ")},` : ",";
4280
+ return `${routerName}.${method}('${path}'${middlewareStr} async (request, reply) => {`;
4281
+ },
4282
+ routeClose() {
4283
+ return [
4284
+ "});"
4285
+ ];
4286
+ },
4287
+ middleware: {
4288
+ policy(args) {
4289
+ return `requirePolicy(${args})`;
4290
+ },
4291
+ bodyParser(tokensExpr) {
4292
+ return `bodyParserMiddleware([${tokensExpr}])`;
4293
+ },
4294
+ signature(args) {
4295
+ return `requireSignature(${args})`;
4296
+ }
4297
+ },
4298
+ request: {
4299
+ params: "request.params",
4300
+ query: "request.query",
4301
+ headers: "request.headers",
4302
+ // ServerKit parses lazily per route, so Fastify's own `request.body` is never populated.
4303
+ parsedBody: "request.parsedBody",
4304
+ // A call, not a property: the raw header carries `; charset=utf-8`, which would match none of
4305
+ // the declared MIME literals the generated switch compares against.
4306
+ contentType: "requestMediaType(request)"
4307
+ },
4308
+ resolveService(className) {
4309
+ return `request.container.get(${className})`;
4310
+ },
4311
+ response: {
4312
+ status(expr) {
4313
+ return `reply.status(${expr});`;
4314
+ },
4315
+ header(name, valueExpr) {
4316
+ return `reply.header('${name}', ${valueExpr});`;
4317
+ },
4318
+ type(expr) {
4319
+ return `reply.type(${expr});`;
4320
+ },
4321
+ send(bodyExpr) {
4322
+ return bodyExpr === void 0 ? [
4323
+ "return reply.send();"
4324
+ ] : [
4325
+ `return reply.send(${bodyExpr});`
4326
+ ];
4327
+ },
4328
+ caseEnd() {
4329
+ return [];
4330
+ }
4331
+ },
4332
+ mcpRouter({ path }) {
4333
+ return `import { type ServerKitRouterType, bodyParserMiddleware, requireSignature, requestHeader } from '${FASTIFY_RUNTIME_MODULE}';
4334
+ import { McpDispatcher, createMcpRequestContext, MCP_AUTH_POLICY } from '@maroonedsoftware/mcp';
4335
+
4336
+ /** Mount the MCP endpoint onto a ServerKit router. Bind \`registerMcpTools\` to the \`McpToolHandlerMap\` token. */
4337
+ export function mountMcp(router: ServerKitRouterType): void {
4338
+ router.post('${path}', bodyParserMiddleware(['json']), requireSignature('mcp', { policy: MCP_AUTH_POLICY }), async (request, reply) => {
4339
+ const dispatcher = request.container.get(McpDispatcher);
4340
+ const context = createMcpRequestContext({ requestId: request.requestId, logger: request.logger });
4341
+ if (dispatcher.sessionMode === 'stateful') {
4342
+ // Fastify's equivalent of Koa's \`ctx.respond = false\`: the dispatcher writes the raw
4343
+ // response itself, and the request scope is disposed on the raw socket close instead.
4344
+ reply.hijack();
4345
+ await dispatcher.dispatchStateful(
4346
+ {
4347
+ req: request.raw,
4348
+ res: reply.raw,
4349
+ body: request.parsedBody,
4350
+ // \`requestHeader\` returns '' for an absent header; the session id is optional.
4351
+ sessionId: requestHeader(request, 'mcp-session-id') || undefined,
4352
+ },
4353
+ context,
4354
+ );
4355
+ return;
4356
+ }
4357
+ const response = await dispatcher.dispatch(JSON.parse(String(request.rawBody)), context);
4358
+ if (response) return reply.send(response);
4359
+ reply.status(202); // a notification \u2014 nothing to return
4360
+ return reply.send();
4361
+ });
4362
+ }
4363
+ `;
4364
+ }
4365
+ };
4366
+
4367
+ // src/server-framework.ts
4368
+ var SERVER_FRAMEWORK_NAMES = [
4369
+ "koa",
4370
+ "fastify"
4371
+ ];
4372
+ var DEFAULT_SERVER_FRAMEWORK_NAME = "koa";
4373
+ var SERVER_FRAMEWORKS = {
4374
+ koa: KOA_SERVER_FRAMEWORK,
4375
+ fastify: FASTIFY_SERVER_FRAMEWORK
4376
+ };
4377
+ function resolveServerFramework(name) {
4378
+ const resolved = name ?? DEFAULT_SERVER_FRAMEWORK_NAME;
4379
+ const framework = SERVER_FRAMEWORKS[resolved];
4380
+ if (!framework) {
4381
+ throw new Error(`plugin-typescript: server.framework '${resolved}' is not supported \u2014 expected one of: ${SERVER_FRAMEWORK_NAMES.join(", ")}.`);
4382
+ }
4383
+ return framework;
4384
+ }
4385
+ __name(resolveServerFramework, "resolveServerFramework");
4386
+
4106
4387
  // src/codegen-mcp.ts
4107
4388
  import { resolveModifiers as resolveModifiers3, emittedResponses as emittedResponses2, toIdentifier as toIdentifier3 } from "@contractkit/core";
4108
4389
  import { basename as basename3, dirname as dirname5, relative as relative5 } from "path";
@@ -4523,29 +4804,10 @@ function generateMcpAggregator(entries) {
4523
4804
  }
4524
4805
  __name(generateMcpAggregator, "generateMcpAggregator");
4525
4806
  function generateMcpRouter(options = {}) {
4526
- const path = options.path ?? "/mcp";
4527
- return `import { ServerKitRouter, bodyParserMiddleware, requireSignature } from '@maroonedsoftware/koa';
4528
- import { McpDispatcher, createMcpRequestContext, MCP_AUTH_POLICY } from '@maroonedsoftware/mcp';
4529
-
4530
- /** Mount the MCP endpoint onto a ServerKit router. Bind \`registerMcpTools\` to the \`McpToolHandlerMap\` token. */
4531
- export function mountMcp(router: ReturnType<typeof ServerKitRouter>): void {
4532
- router.post('${path}', bodyParserMiddleware(['json']), requireSignature('mcp', { policy: MCP_AUTH_POLICY }), async (ctx) => {
4533
- const dispatcher = ctx.container.get(McpDispatcher);
4534
- const context = createMcpRequestContext({ requestId: ctx.requestId, logger: ctx.logger });
4535
- if (dispatcher.sessionMode === 'stateful') {
4536
- ctx.respond = false;
4537
- await dispatcher.dispatchStateful(
4538
- { req: ctx.req, res: ctx.res, body: ctx.parsedBody, sessionId: ctx.get('mcp-session-id') },
4539
- context,
4540
- );
4541
- } else {
4542
- const response = await dispatcher.dispatch(JSON.parse(String(ctx.rawBody)), context);
4543
- if (response) ctx.body = response;
4544
- else ctx.status = 202; // a notification \u2014 nothing to return
4545
- }
4546
- });
4547
- }
4548
- `;
4807
+ const framework = options.framework ?? KOA_SERVER_FRAMEWORK;
4808
+ return framework.mcpRouter({
4809
+ path: options.path ?? "/mcp"
4810
+ });
4549
4811
  }
4550
4812
  __name(generateMcpRouter, "generateMcpRouter");
4551
4813
 
@@ -4779,6 +5041,10 @@ function createTypescriptPlugin(config, rootDir) {
4779
5041
  }
4780
5042
  __name(createTypescriptPlugin, "createTypescriptPlugin");
4781
5043
  function assertValidConfig(config) {
5044
+ const framework = config.server?.framework;
5045
+ if (framework !== void 0 && !SERVER_FRAMEWORK_NAMES.includes(framework)) {
5046
+ throw new Error(`plugin-typescript: server.framework '${String(framework)}' is not supported \u2014 expected one of: ${SERVER_FRAMEWORK_NAMES.join(", ")}.`);
5047
+ }
4782
5048
  if (config.server?.validateResponses && !config.server.zod) {
4783
5049
  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.");
4784
5050
  }
@@ -4908,6 +5174,7 @@ function sliceModelSet(refs, ownNames, set) {
4908
5174
  __name(sliceModelSet, "sliceModelSet");
4909
5175
  function collectServerOutput(config, rootDir, inputs, units) {
4910
5176
  const serverBase = resolve2(rootDir, config.baseDir ?? ".");
5177
+ const framework = resolveServerFramework(config.framework);
4911
5178
  const modelsWithInput = inputs.modelsWithInput;
4912
5179
  const modelsWithOutput = inputs.modelsWithOutput;
4913
5180
  const modelsWithTransform = computeModelsWithCaseTransform(inputs.contractRoots.flatMap((r) => r.models));
@@ -4956,7 +5223,7 @@ function collectServerOutput(config, rootDir, inputs, units) {
4956
5223
  currentOutPath: typeOutPath,
4957
5224
  modelsWithInput,
4958
5225
  modelsWithOutput,
4959
- // These types are consumed by Koa handlers, so `binary` is a Buffer, not a Blob.
5226
+ // These types are consumed by server handlers, so `binary` is a Buffer, not a Blob.
4960
5227
  target: "server"
4961
5228
  };
4962
5229
  const content = config.zod ? generateContract(ast, renderCtx) : generatePlainTypes(ast, renderCtx);
@@ -4987,6 +5254,9 @@ function collectServerOutput(config, rootDir, inputs, units) {
4987
5254
  // this router's output with no change to `root` or the config.
4988
5255
  modelsWithTransform: sliceModelSet(refs, /* @__PURE__ */ new Set(), modelsWithTransform),
4989
5256
  validateResponses: config.validateResponses ?? false,
5257
+ // Covered by `sub` already, which is the whole sub-config; explicit for the same reason
5258
+ // `validateResponses` is — the inputs that change a router's text read at a glance.
5259
+ framework: framework.name,
4990
5260
  sub: subConfigKey
4991
5261
  });
4992
5262
  units.push({
@@ -5003,7 +5273,8 @@ function collectServerOutput(config, rootDir, inputs, units) {
5003
5273
  modelsWithOutput,
5004
5274
  modelsWithTransform,
5005
5275
  includeInternal: config.includeInternal,
5006
- validateResponses: config.validateResponses
5276
+ validateResponses: config.validateResponses,
5277
+ framework
5007
5278
  })
5008
5279
  }
5009
5280
  ], "render")
@@ -5625,10 +5896,12 @@ function collectMcpOutput(config, fullConfig, rootDir, inputs, units, globalFile
5625
5896
  });
5626
5897
  if (config.emitRouter !== false) {
5627
5898
  const routerPath = join2(mcpBase, config.output?.router ?? "mcp.router.ts");
5899
+ const framework = resolveServerFramework(fullConfig.server?.framework);
5628
5900
  globalFiles.push({
5629
5901
  relativePath: routerPath,
5630
5902
  content: generateMcpRouter({
5631
- path: config.path
5903
+ path: config.path,
5904
+ framework
5632
5905
  })
5633
5906
  });
5634
5907
  }
@@ -5686,8 +5959,14 @@ function stableSubConfig(config) {
5686
5959
  }
5687
5960
  __name(stableSubConfig, "stableSubConfig");
5688
5961
  export {
5962
+ DEFAULT_SERVER_FRAMEWORK_NAME,
5963
+ FASTIFY_SERVER_FRAMEWORK,
5964
+ KOA_SERVER_FRAMEWORK,
5965
+ SERVER_FRAMEWORKS,
5966
+ SERVER_FRAMEWORK_NAMES,
5689
5967
  TYPESCRIPT_CODEGEN_VERSION,
5690
5968
  createTypescriptPlugin,
5691
- index_default as default
5969
+ index_default as default,
5970
+ resolveServerFramework
5692
5971
  };
5693
5972
  //# sourceMappingURL=index.js.map