@cubis/foundry 0.3.49 → 0.3.51

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/mcp/Dockerfile CHANGED
@@ -16,4 +16,5 @@ COPY --from=build /app/dist ./dist
16
16
  COPY --from=build /app/config.json ./config.json
17
17
 
18
18
  EXPOSE 3100
19
+ ENV LOG_LEVEL=info
19
20
  ENTRYPOINT ["node", "dist/index.js", "--transport", "http"]
package/mcp/dist/index.js CHANGED
@@ -54,7 +54,10 @@ var LEVEL_ORDER = {
54
54
  warn: 2,
55
55
  error: 3
56
56
  };
57
- var currentLevel = "info";
57
+ var currentLevel = process.env.LOG_LEVEL?.toLowerCase() || "info";
58
+ if (!LEVEL_ORDER[currentLevel]) {
59
+ currentLevel = "info";
60
+ }
58
61
  function setLogLevel(level) {
59
62
  currentLevel = level;
60
63
  }
@@ -148,6 +151,10 @@ async function scanVaultRoots(roots, basePath) {
148
151
  const skillFile = path2.join(entryPath, "SKILL.md");
149
152
  const skillStat = await stat(skillFile).catch(() => null);
150
153
  if (!skillStat?.isFile()) continue;
154
+ if (skillStat.size === 0) {
155
+ logger.warn(`Skipping empty SKILL.md: ${skillFile}`);
156
+ continue;
157
+ }
151
158
  const wrapperKind = await detectWrapperKind(entry, skillFile);
152
159
  if (wrapperKind) {
153
160
  logger.debug(
@@ -357,15 +364,20 @@ function buildSkillToolMetrics({
357
364
  fullCatalogEstimatedTokens,
358
365
  responseEstimatedTokens,
359
366
  selectedSkillsEstimatedTokens = null,
360
- loadedSkillEstimatedTokens = null
367
+ loadedSkillEstimatedTokens = null,
368
+ responseCharacterCount = 0
361
369
  }) {
362
370
  const usedEstimatedTokens = loadedSkillEstimatedTokens ?? selectedSkillsEstimatedTokens ?? responseEstimatedTokens;
363
- const savings = estimateSavings(fullCatalogEstimatedTokens, usedEstimatedTokens);
371
+ const savings = estimateSavings(
372
+ fullCatalogEstimatedTokens,
373
+ usedEstimatedTokens
374
+ );
364
375
  return {
365
376
  estimatorVersion: TOKEN_ESTIMATOR_VERSION,
366
377
  charsPerToken: normalizeCharsPerToken(charsPerToken),
367
378
  fullCatalogEstimatedTokens: Math.max(0, fullCatalogEstimatedTokens),
368
379
  responseEstimatedTokens: Math.max(0, responseEstimatedTokens),
380
+ responseCharacterCount: Math.max(0, responseCharacterCount),
369
381
  selectedSkillsEstimatedTokens: selectedSkillsEstimatedTokens === null ? null : Math.max(0, selectedSkillsEstimatedTokens),
370
382
  loadedSkillEstimatedTokens: loadedSkillEstimatedTokens === null ? null : Math.max(0, loadedSkillEstimatedTokens),
371
383
  estimatedSavingsVsFullCatalog: savings.estimatedSavingsTokens,
@@ -485,11 +497,23 @@ async function collectSiblingMarkdownTargets(skillDir) {
485
497
  );
486
498
  const targets = [];
487
499
  for (const entry of entries) {
488
- if (!entry.isFile()) continue;
489
500
  if (entry.name.startsWith(".")) continue;
490
- if (!entry.name.toLowerCase().endsWith(".md")) continue;
491
- if (entry.name.toLowerCase() === "skill.md") continue;
492
- targets.push(entry.name);
501
+ if (entry.isDirectory()) {
502
+ const subEntries = await readdir2(path3.join(skillDir, entry.name), {
503
+ withFileTypes: true
504
+ }).catch(() => []);
505
+ for (const sub of subEntries) {
506
+ if (!sub.isFile()) continue;
507
+ if (sub.name.startsWith(".")) continue;
508
+ if (!sub.name.toLowerCase().endsWith(".md")) continue;
509
+ targets.push(`${entry.name}/${sub.name}`);
510
+ if (targets.length >= MAX_REFERENCED_FILES) break;
511
+ }
512
+ } else if (entry.isFile()) {
513
+ if (!entry.name.toLowerCase().endsWith(".md")) continue;
514
+ if (entry.name.toLowerCase() === "skill.md") continue;
515
+ targets.push(entry.name);
516
+ }
493
517
  if (targets.length >= MAX_REFERENCED_FILES) break;
494
518
  }
495
519
  targets.sort((a, b) => a.localeCompare(b));
@@ -536,7 +560,8 @@ function handleSkillListCategories(manifest, charsPerToken) {
536
560
  const metrics = buildSkillToolMetrics({
537
561
  charsPerToken,
538
562
  fullCatalogEstimatedTokens: manifest.fullCatalogEstimatedTokens,
539
- responseEstimatedTokens: estimateTokensFromText(text, charsPerToken)
563
+ responseEstimatedTokens: estimateTokensFromText(text, charsPerToken),
564
+ responseCharacterCount: text.length
540
565
  });
541
566
  return {
542
567
  content: [
@@ -547,6 +572,9 @@ function handleSkillListCategories(manifest, charsPerToken) {
547
572
  ],
548
573
  structuredContent: {
549
574
  metrics
575
+ },
576
+ _meta: {
577
+ metrics
550
578
  }
551
579
  };
552
580
  }
@@ -602,7 +630,8 @@ async function handleSkillBrowseCategory(args, manifest, summaryMaxLength, chars
602
630
  charsPerToken,
603
631
  fullCatalogEstimatedTokens: manifest.fullCatalogEstimatedTokens,
604
632
  responseEstimatedTokens: estimateTokensFromText(text, charsPerToken),
605
- selectedSkillsEstimatedTokens
633
+ selectedSkillsEstimatedTokens,
634
+ responseCharacterCount: text.length
606
635
  });
607
636
  return {
608
637
  content: [
@@ -613,6 +642,9 @@ async function handleSkillBrowseCategory(args, manifest, summaryMaxLength, chars
613
642
  ],
614
643
  structuredContent: {
615
644
  metrics
645
+ },
646
+ _meta: {
647
+ metrics
616
648
  }
617
649
  };
618
650
  }
@@ -658,7 +690,8 @@ async function handleSkillSearch(args, manifest, summaryMaxLength, charsPerToken
658
690
  charsPerToken,
659
691
  fullCatalogEstimatedTokens: manifest.fullCatalogEstimatedTokens,
660
692
  responseEstimatedTokens: estimateTokensFromText(text, charsPerToken),
661
- selectedSkillsEstimatedTokens
693
+ selectedSkillsEstimatedTokens,
694
+ responseCharacterCount: text.length
662
695
  });
663
696
  return {
664
697
  content: [
@@ -669,6 +702,9 @@ async function handleSkillSearch(args, manifest, summaryMaxLength, charsPerToken
669
702
  ],
670
703
  structuredContent: {
671
704
  metrics
705
+ },
706
+ _meta: {
707
+ metrics
672
708
  }
673
709
  };
674
710
  }
@@ -710,6 +746,11 @@ async function handleSkillGet(args, manifest, charsPerToken) {
710
746
  ])
711
747
  ].join("\n") : "";
712
748
  const content = `${skillContent}${referenceSection}`;
749
+ if (content.trim().length === 0) {
750
+ invalidInput(
751
+ `Skill "${id}" has empty content (SKILL.md is empty or whitespace-only). This skill may be corrupt or incomplete.`
752
+ );
753
+ }
713
754
  const loadedSkillEstimatedTokens = estimateTokensFromText(
714
755
  content,
715
756
  charsPerToken
@@ -718,7 +759,8 @@ async function handleSkillGet(args, manifest, charsPerToken) {
718
759
  charsPerToken,
719
760
  fullCatalogEstimatedTokens: manifest.fullCatalogEstimatedTokens,
720
761
  responseEstimatedTokens: loadedSkillEstimatedTokens,
721
- loadedSkillEstimatedTokens
762
+ loadedSkillEstimatedTokens,
763
+ responseCharacterCount: content.length
722
764
  });
723
765
  return {
724
766
  content: [
@@ -730,6 +772,10 @@ async function handleSkillGet(args, manifest, charsPerToken) {
730
772
  structuredContent: {
731
773
  references: references.map((ref) => ({ path: ref.relativePath })),
732
774
  metrics
775
+ },
776
+ _meta: {
777
+ references: references.map((ref) => ({ path: ref.relativePath })),
778
+ metrics
733
779
  }
734
780
  };
735
781
  }
@@ -755,7 +801,10 @@ function handleSkillBudgetReport(args, manifest, charsPerToken) {
755
801
  return {
756
802
  id: skill.id,
757
803
  category: skill.category,
758
- estimatedTokens: estimateTokensFromBytes(skill.fileBytes, charsPerToken)
804
+ estimatedTokens: estimateTokensFromBytes(
805
+ skill.fileBytes,
806
+ charsPerToken
807
+ )
759
808
  };
760
809
  }).filter((item) => Boolean(item));
761
810
  const loadedSkills = loadedSkillIds.map((id) => {
@@ -764,13 +813,18 @@ function handleSkillBudgetReport(args, manifest, charsPerToken) {
764
813
  return {
765
814
  id: skill.id,
766
815
  category: skill.category,
767
- estimatedTokens: estimateTokensFromBytes(skill.fileBytes, charsPerToken)
816
+ estimatedTokens: estimateTokensFromBytes(
817
+ skill.fileBytes,
818
+ charsPerToken
819
+ )
768
820
  };
769
821
  }).filter((item) => Boolean(item));
770
822
  const unknownSelectedSkillIds = selectedSkillIds.filter(
771
823
  (id) => !skillById.has(id)
772
824
  );
773
- const unknownLoadedSkillIds = loadedSkillIds.filter((id) => !skillById.has(id));
825
+ const unknownLoadedSkillIds = loadedSkillIds.filter(
826
+ (id) => !skillById.has(id)
827
+ );
774
828
  const selectedSkillsEstimatedTokens = selectedSkills.reduce(
775
829
  (sum, skill) => sum + skill.estimatedTokens,
776
830
  0
@@ -786,7 +840,9 @@ function handleSkillBudgetReport(args, manifest, charsPerToken) {
786
840
  );
787
841
  const selectedIdSet = new Set(selectedSkills.map((skill) => skill.id));
788
842
  const loadedIdSet = new Set(loadedSkills.map((skill) => skill.id));
789
- const skippedSkills = manifest.skills.filter((skill) => !selectedIdSet.has(skill.id) && !loadedIdSet.has(skill.id)).map((skill) => skill.id).sort((a, b) => a.localeCompare(b));
843
+ const skippedSkills = manifest.skills.filter(
844
+ (skill) => !selectedIdSet.has(skill.id) && !loadedIdSet.has(skill.id)
845
+ ).map((skill) => skill.id).sort((a, b) => a.localeCompare(b));
790
846
  const payload = {
791
847
  skillLog: {
792
848
  selectedSkills,
@@ -806,14 +862,16 @@ function handleSkillBudgetReport(args, manifest, charsPerToken) {
806
862
  estimated: true
807
863
  }
808
864
  };
865
+ const text = JSON.stringify(payload, null, 2);
809
866
  return {
810
867
  content: [
811
868
  {
812
869
  type: "text",
813
- text: JSON.stringify(payload, null, 2)
870
+ text
814
871
  }
815
872
  ],
816
- structuredContent: payload
873
+ structuredContent: payload,
874
+ _meta: payload
817
875
  };
818
876
  }
819
877
 
@@ -1728,10 +1786,13 @@ async function createServer({
1728
1786
  };
1729
1787
  for (const entry of TOOL_REGISTRY) {
1730
1788
  const handler = entry.createHandler(runtimeCtx);
1731
- server.tool(
1789
+ server.registerTool(
1732
1790
  entry.name,
1733
- entry.description,
1734
- entry.schema.shape,
1791
+ {
1792
+ description: entry.description,
1793
+ inputSchema: entry.schema,
1794
+ annotations: {}
1795
+ },
1735
1796
  handler
1736
1797
  );
1737
1798
  }
@@ -1739,14 +1800,17 @@ async function createServer({
1739
1800
  `Registered ${TOOL_REGISTRY.length} built-in tools from registry`
1740
1801
  );
1741
1802
  const upstreamCatalogs = await discoverUpstreamCatalogs();
1742
- const dynamicArgsShape = z13.object({}).passthrough().shape;
1803
+ const dynamicSchema = z13.object({}).passthrough();
1743
1804
  for (const catalog of [upstreamCatalogs.postman, upstreamCatalogs.stitch]) {
1744
1805
  for (const tool of catalog.tools) {
1745
1806
  const namespaced = tool.namespacedName;
1746
- server.tool(
1807
+ server.registerTool(
1747
1808
  namespaced,
1748
- `[${catalog.service} passthrough] ${tool.description || tool.name}`,
1749
- dynamicArgsShape,
1809
+ {
1810
+ description: `[${catalog.service} passthrough] ${tool.description || tool.name}`,
1811
+ inputSchema: dynamicSchema,
1812
+ annotations: {}
1813
+ },
1750
1814
  async (args) => {
1751
1815
  try {
1752
1816
  const result = await callUpstreamTool({
@@ -1781,27 +1845,177 @@ function createStdioTransport() {
1781
1845
  }
1782
1846
 
1783
1847
  // src/transports/streamableHttp.ts
1784
- import { createServer as createServer2 } from "http";
1848
+ import {
1849
+ createServer as createServer2
1850
+ } from "http";
1785
1851
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1786
- function createStreamableHttpTransport(options) {
1787
- const transport = new StreamableHTTPServerTransport({
1788
- sessionIdGenerator: () => crypto.randomUUID()
1852
+ var SESSION_TTL_MS = 30 * 60 * 1e3;
1853
+ var CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
1854
+ function readBody(req) {
1855
+ return new Promise((resolve, reject) => {
1856
+ const chunks = [];
1857
+ req.on("data", (c) => chunks.push(c));
1858
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
1859
+ req.on("error", reject);
1789
1860
  });
1790
- const httpServer = createServer2(async (req, res) => {
1791
- const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
1792
- if (url.pathname !== "/mcp") {
1793
- res.writeHead(404, { "Content-Type": "text/plain" });
1794
- res.end("Not Found");
1795
- return;
1861
+ }
1862
+ function createMultiSessionHttpServer(options, serverFactory) {
1863
+ const sessions = /* @__PURE__ */ new Map();
1864
+ const cleanupTimer = setInterval(() => {
1865
+ const now = Date.now();
1866
+ for (const [id, entry] of sessions) {
1867
+ if (now - entry.lastActivity > SESSION_TTL_MS) {
1868
+ logger.info(
1869
+ `Session ${id.slice(0, 8)} expired after idle (active: ${sessions.size - 1})`
1870
+ );
1871
+ entry.transport.close().catch(() => {
1872
+ });
1873
+ entry.server.close().catch(() => {
1874
+ });
1875
+ sessions.delete(id);
1876
+ }
1796
1877
  }
1797
- await transport.handleRequest(req, res);
1798
- });
1878
+ }, CLEANUP_INTERVAL_MS);
1879
+ cleanupTimer.unref();
1880
+ const httpServer = createServer2(
1881
+ async (req, res) => {
1882
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
1883
+ if (url.pathname !== "/mcp") {
1884
+ res.writeHead(404, { "Content-Type": "text/plain" });
1885
+ res.end("Not Found");
1886
+ return;
1887
+ }
1888
+ if (req.method === "DELETE") {
1889
+ const sid = req.headers["mcp-session-id"];
1890
+ if (sid && sessions.has(sid)) {
1891
+ const entry = sessions.get(sid);
1892
+ await entry.transport.close().catch(() => {
1893
+ });
1894
+ await entry.server.close().catch(() => {
1895
+ });
1896
+ sessions.delete(sid);
1897
+ logger.info(
1898
+ `Session ${sid.slice(0, 8)} terminated (active: ${sessions.size})`
1899
+ );
1900
+ res.writeHead(200, { "Content-Type": "text/plain" });
1901
+ res.end("Session closed");
1902
+ } else {
1903
+ res.writeHead(404, { "Content-Type": "text/plain" });
1904
+ res.end("Session not found");
1905
+ }
1906
+ return;
1907
+ }
1908
+ if (req.method === "GET") {
1909
+ const sid = req.headers["mcp-session-id"];
1910
+ if (sid && sessions.has(sid)) {
1911
+ const entry = sessions.get(sid);
1912
+ entry.lastActivity = Date.now();
1913
+ await entry.transport.handleRequest(req, res);
1914
+ } else {
1915
+ res.writeHead(400, { "Content-Type": "text/plain" });
1916
+ res.end("Missing or invalid mcp-session-id");
1917
+ }
1918
+ return;
1919
+ }
1920
+ if (req.method === "POST") {
1921
+ const sid = req.headers["mcp-session-id"];
1922
+ if (sid && sessions.has(sid)) {
1923
+ const entry = sessions.get(sid);
1924
+ entry.lastActivity = Date.now();
1925
+ await entry.transport.handleRequest(req, res);
1926
+ return;
1927
+ }
1928
+ const rawBody = await readBody(req);
1929
+ let parsed;
1930
+ try {
1931
+ parsed = JSON.parse(rawBody);
1932
+ } catch {
1933
+ logger.warn(`Bad JSON in POST from ${req.socket.remoteAddress}`);
1934
+ res.writeHead(400, { "Content-Type": "application/json" });
1935
+ res.end(
1936
+ JSON.stringify({
1937
+ jsonrpc: "2.0",
1938
+ error: { code: -32700, message: "Parse error" },
1939
+ id: null
1940
+ })
1941
+ );
1942
+ return;
1943
+ }
1944
+ const isInit = parsed && typeof parsed === "object" && "method" in parsed && parsed.method === "initialize";
1945
+ if (!isInit) {
1946
+ logger.warn(
1947
+ `POST without session: method=${parsed?.method ?? "unknown"}`
1948
+ );
1949
+ res.writeHead(400, { "Content-Type": "application/json" });
1950
+ res.end(
1951
+ JSON.stringify({
1952
+ jsonrpc: "2.0",
1953
+ error: {
1954
+ code: -32600,
1955
+ message: "Invalid Request: missing or unknown mcp-session-id"
1956
+ },
1957
+ id: parsed?.id ?? null
1958
+ })
1959
+ );
1960
+ return;
1961
+ }
1962
+ const transport = new StreamableHTTPServerTransport({
1963
+ sessionIdGenerator: () => crypto.randomUUID()
1964
+ });
1965
+ try {
1966
+ const server = await serverFactory(transport);
1967
+ await transport.handleRequest(req, res, parsed);
1968
+ const sessionId = transport.sessionId;
1969
+ if (sessionId) {
1970
+ sessions.set(sessionId, {
1971
+ transport,
1972
+ server,
1973
+ lastActivity: Date.now()
1974
+ });
1975
+ logger.info(
1976
+ `New session ${sessionId.slice(0, 8)} (active: ${sessions.size})`
1977
+ );
1978
+ }
1979
+ } catch (error) {
1980
+ logger.error(`Failed to create MCP session: ${error}`);
1981
+ if (!res.headersSent) {
1982
+ res.writeHead(500, { "Content-Type": "application/json" });
1983
+ res.end(
1984
+ JSON.stringify({
1985
+ jsonrpc: "2.0",
1986
+ error: {
1987
+ code: -32603,
1988
+ message: "Internal error creating session"
1989
+ },
1990
+ id: parsed?.id ?? null
1991
+ })
1992
+ );
1993
+ }
1994
+ }
1995
+ return;
1996
+ }
1997
+ res.writeHead(405, { "Content-Type": "text/plain" });
1998
+ res.end("Method Not Allowed");
1999
+ }
2000
+ );
1799
2001
  httpServer.listen(options.port, options.host, () => {
1800
2002
  logger.info(
1801
- `Streamable HTTP transport listening on http://${options.host}:${options.port}/mcp`
2003
+ `Streamable HTTP transport listening on http://${options.host}:${options.port}/mcp (multi-session)`
1802
2004
  );
1803
2005
  });
1804
- return { transport, httpServer };
2006
+ async function closeAll() {
2007
+ clearInterval(cleanupTimer);
2008
+ for (const [id, entry] of sessions) {
2009
+ await entry.transport.close().catch(() => {
2010
+ });
2011
+ await entry.server.close().catch(() => {
2012
+ });
2013
+ sessions.delete(id);
2014
+ logger.debug(`Closed session ${id} during shutdown`);
2015
+ }
2016
+ httpServer.close();
2017
+ }
2018
+ return { httpServer, closeAll };
1805
2019
  }
1806
2020
 
1807
2021
  // src/index.ts
@@ -1833,13 +2047,17 @@ function parseArgs(argv) {
1833
2047
  if (val === "auto" || val === "global" || val === "project") {
1834
2048
  scope = val;
1835
2049
  } else {
1836
- logger.error(`Unknown scope: ${val}. Use "auto", "global", or "project".`);
2050
+ logger.error(
2051
+ `Unknown scope: ${val}. Use "auto", "global", or "project".`
2052
+ );
1837
2053
  process.exit(1);
1838
2054
  }
1839
2055
  } else if (arg === "--port" && argv[i + 1]) {
1840
2056
  const val = Number.parseInt(argv[++i], 10);
1841
2057
  if (!Number.isInteger(val) || val <= 0 || val > 65535) {
1842
- logger.error(`Invalid port: ${argv[i]}. Use an integer from 1 to 65535.`);
2058
+ logger.error(
2059
+ `Invalid port: ${argv[i]}. Use an integer from 1 to 65535.`
2060
+ );
1843
2061
  process.exit(1);
1844
2062
  }
1845
2063
  port = val;
@@ -1928,27 +2146,37 @@ async function main() {
1928
2146
  transportName
1929
2147
  );
1930
2148
  printConfigStatus(args.scope);
1931
- const mcpServer = await createServer({
1932
- config: serverConfig,
1933
- manifest,
1934
- defaultConfigScope: args.scope
1935
- });
1936
2149
  if (args.transport === "http") {
1937
2150
  const httpOpts = {
1938
2151
  port: resolvedHttpPort,
1939
2152
  host: args.host ?? serverConfig.transport.http?.host ?? "127.0.0.1"
1940
2153
  };
1941
- const { transport, httpServer } = createStreamableHttpTransport(httpOpts);
1942
- await mcpServer.connect(transport);
2154
+ const serverFactory = async (transport) => {
2155
+ const server = await createServer({
2156
+ config: serverConfig,
2157
+ manifest,
2158
+ defaultConfigScope: args.scope
2159
+ });
2160
+ await server.connect(transport);
2161
+ return server;
2162
+ };
2163
+ const { httpServer, closeAll } = createMultiSessionHttpServer(
2164
+ httpOpts,
2165
+ serverFactory
2166
+ );
1943
2167
  const shutdown = async () => {
1944
2168
  logger.info("Shutting down HTTP transport...");
1945
- httpServer.close();
1946
- await mcpServer.close();
2169
+ await closeAll();
1947
2170
  process.exit(0);
1948
2171
  };
1949
2172
  process.on("SIGINT", shutdown);
1950
2173
  process.on("SIGTERM", shutdown);
1951
2174
  } else {
2175
+ const mcpServer = await createServer({
2176
+ config: serverConfig,
2177
+ manifest,
2178
+ defaultConfigScope: args.scope
2179
+ });
1952
2180
  const transport = createStdioTransport();
1953
2181
  await mcpServer.connect(transport);
1954
2182
  const shutdown = async () => {
package/mcp/src/index.ts CHANGED
@@ -16,7 +16,10 @@ import { scanVaultRoots } from "./vault/scanner.js";
16
16
  import { buildManifest, enrichWithDescriptions } from "./vault/manifest.js";
17
17
  import { createServer } from "./server.js";
18
18
  import { createStdioTransport } from "./transports/stdio.js";
19
- import { createStreamableHttpTransport } from "./transports/streamableHttp.js";
19
+ import {
20
+ createMultiSessionHttpServer,
21
+ type McpServerFactory,
22
+ } from "./transports/streamableHttp.js";
20
23
  import { logger, setLogLevel } from "./utils/logger.js";
21
24
  import {
22
25
  parsePostmanState,
@@ -65,13 +68,17 @@ function parseArgs(argv: string[]): {
65
68
  if (val === "auto" || val === "global" || val === "project") {
66
69
  scope = val;
67
70
  } else {
68
- logger.error(`Unknown scope: ${val}. Use "auto", "global", or "project".`);
71
+ logger.error(
72
+ `Unknown scope: ${val}. Use "auto", "global", or "project".`,
73
+ );
69
74
  process.exit(1);
70
75
  }
71
76
  } else if (arg === "--port" && argv[i + 1]) {
72
77
  const val = Number.parseInt(argv[++i], 10);
73
78
  if (!Number.isInteger(val) || val <= 0 || val > 65535) {
74
- logger.error(`Invalid port: ${argv[i]}. Use an integer from 1 to 65535.`);
79
+ logger.error(
80
+ `Invalid port: ${argv[i]}. Use an integer from 1 to 65535.`,
81
+ );
75
82
  process.exit(1);
76
83
  }
77
84
  port = val;
@@ -182,7 +189,8 @@ async function main(): Promise<void> {
182
189
  }
183
190
 
184
191
  // Print startup banner
185
- const resolvedHttpPort = args.port ?? serverConfig.transport.http?.port ?? 3100;
192
+ const resolvedHttpPort =
193
+ args.port ?? serverConfig.transport.http?.port ?? 3100;
186
194
  const transportName =
187
195
  args.transport === "http"
188
196
  ? `Streamable HTTP :${resolvedHttpPort}`
@@ -194,32 +202,46 @@ async function main(): Promise<void> {
194
202
  );
195
203
  printConfigStatus(args.scope);
196
204
 
197
- // Create MCP server
198
- const mcpServer = await createServer({
199
- config: serverConfig,
200
- manifest,
201
- defaultConfigScope: args.scope,
202
- });
203
-
204
205
  // Connect transport
205
206
  if (args.transport === "http") {
206
207
  const httpOpts = {
207
208
  port: resolvedHttpPort,
208
209
  host: args.host ?? serverConfig.transport.http?.host ?? "127.0.0.1",
209
210
  };
210
- const { transport, httpServer } = createStreamableHttpTransport(httpOpts);
211
- await mcpServer.connect(transport);
211
+
212
+ // Multi-session architecture: the factory is called once per initialize
213
+ // handshake. Vault/manifest/config are shared; only McpServer + tools
214
+ // registration is per-session (lightweight).
215
+ const serverFactory: McpServerFactory = async (transport) => {
216
+ const server = await createServer({
217
+ config: serverConfig,
218
+ manifest,
219
+ defaultConfigScope: args.scope,
220
+ });
221
+ await server.connect(transport);
222
+ return server;
223
+ };
224
+
225
+ const { httpServer, closeAll } = createMultiSessionHttpServer(
226
+ httpOpts,
227
+ serverFactory,
228
+ );
212
229
 
213
230
  // Graceful shutdown
214
231
  const shutdown = async () => {
215
232
  logger.info("Shutting down HTTP transport...");
216
- httpServer.close();
217
- await mcpServer.close();
233
+ await closeAll();
218
234
  process.exit(0);
219
235
  };
220
236
  process.on("SIGINT", shutdown);
221
237
  process.on("SIGTERM", shutdown);
222
238
  } else {
239
+ // stdio is single-session; create one McpServer directly.
240
+ const mcpServer = await createServer({
241
+ config: serverConfig,
242
+ manifest,
243
+ defaultConfigScope: args.scope,
244
+ });
223
245
  const transport = createStdioTransport();
224
246
  await mcpServer.connect(transport);
225
247
 
package/mcp/src/server.ts CHANGED
@@ -72,14 +72,17 @@ export async function createServer({
72
72
 
73
73
  for (const entry of TOOL_REGISTRY) {
74
74
  const handler = entry.createHandler(runtimeCtx);
75
- // Cast is safe: registry handler signatures are compatible at runtime.
76
- // The overload ambiguity arises because an empty ZodRawShape `{}`
77
- // is structurally assignable to the annotations object overload.
78
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
- (server as any).tool(
75
+ // Use registerTool() with explicit config object avoids the
76
+ // overload ambiguity in the deprecated tool() where empty `{}`
77
+ // schemas can be misinterpreted as annotations, causing tools
78
+ // to appear "not exposed" on some clients (e.g. Codex, Gemini).
79
+ server.registerTool(
80
80
  entry.name,
81
- entry.description,
82
- entry.schema.shape,
81
+ {
82
+ description: entry.description,
83
+ inputSchema: entry.schema,
84
+ annotations: {},
85
+ },
83
86
  handler,
84
87
  );
85
88
  }
@@ -90,16 +93,18 @@ export async function createServer({
90
93
 
91
94
  // ─── Dynamic upstream passthrough tools ────────────────────
92
95
  const upstreamCatalogs = await discoverUpstreamCatalogs();
93
- const dynamicArgsShape = z.object({}).passthrough().shape;
96
+ const dynamicSchema = z.object({}).passthrough();
94
97
 
95
98
  for (const catalog of [upstreamCatalogs.postman, upstreamCatalogs.stitch]) {
96
99
  for (const tool of catalog.tools) {
97
100
  const namespaced = tool.namespacedName;
98
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
- (server as any).tool(
101
+ server.registerTool(
100
102
  namespaced,
101
- `[${catalog.service} passthrough] ${tool.description || tool.name}`,
102
- dynamicArgsShape,
103
+ {
104
+ description: `[${catalog.service} passthrough] ${tool.description || tool.name}`,
105
+ inputSchema: dynamicSchema,
106
+ annotations: {},
107
+ },
103
108
  async (args: Record<string, unknown>) => {
104
109
  try {
105
110
  const result = await callUpstreamTool({