@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/README.md +5 -0
- package/bin/cubis.js +470 -107
- package/mcp/Dockerfile +1 -0
- package/mcp/dist/index.js +277 -49
- package/mcp/src/index.ts +37 -15
- package/mcp/src/server.ts +17 -12
- package/mcp/src/telemetry/tokenBudget.ts +8 -2
- package/mcp/src/tools/skillBrowseCategory.ts +6 -1
- package/mcp/src/tools/skillBudgetReport.ts +18 -5
- package/mcp/src/tools/skillGet.ts +12 -0
- package/mcp/src/tools/skillListCategories.ts +4 -0
- package/mcp/src/tools/skillSearch.ts +6 -1
- package/mcp/src/transports/streamableHttp.ts +247 -4
- package/mcp/src/utils/logger.ts +5 -1
- package/mcp/src/vault/manifest.ts +22 -5
- package/mcp/src/vault/scanner.ts +16 -3
- package/package.json +1 -1
- package/workflows/skills/postman/SKILL.md +67 -4
- package/workflows/workflows/agent-environment-setup/platforms/antigravity/rules/GEMINI.md +10 -1
- package/workflows/workflows/agent-environment-setup/platforms/codex/rules/AGENTS.md +10 -1
- package/workflows/workflows/agent-environment-setup/platforms/copilot/rules/AGENTS.md +10 -1
- package/workflows/workflows/agent-environment-setup/platforms/copilot/rules/copilot-instructions.md +10 -1
- package/workflows/workflows/agent-environment-setup/platforms/copilot/skills/postman/SKILL.md +67 -4
- package/workflows/workflows/agent-environment-setup/platforms/cursor/skills/postman/SKILL.md +67 -4
- package/workflows/workflows/agent-environment-setup/platforms/windsurf/skills/postman/SKILL.md +67 -4
package/mcp/Dockerfile
CHANGED
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(
|
|
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 (
|
|
491
|
-
|
|
492
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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.
|
|
1789
|
+
server.registerTool(
|
|
1732
1790
|
entry.name,
|
|
1733
|
-
|
|
1734
|
-
|
|
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
|
|
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.
|
|
1807
|
+
server.registerTool(
|
|
1747
1808
|
namespaced,
|
|
1748
|
-
|
|
1749
|
-
|
|
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 {
|
|
1848
|
+
import {
|
|
1849
|
+
createServer as createServer2
|
|
1850
|
+
} from "http";
|
|
1785
1851
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
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
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
1942
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
|
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
|
-
|
|
99
|
-
(server as any).tool(
|
|
101
|
+
server.registerTool(
|
|
100
102
|
namespaced,
|
|
101
|
-
|
|
102
|
-
|
|
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({
|