@cubis/foundry 0.3.46 → 0.3.48

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/src/server.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Cubis Foundry MCP Server – server factory.
3
3
  *
4
- * Creates and configures the McpServer instance with built-in tools plus
5
- * dynamic Postman/Stitch passthrough namespaces.
4
+ * Creates and configures the McpServer instance with built-in tools
5
+ * (via declarative registry) plus dynamic Postman/Stitch passthrough namespaces.
6
6
  */
7
7
 
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -10,59 +10,12 @@ import { z } from "zod";
10
10
  import type { ServerConfig } from "./config/schema.js";
11
11
  import type { ConfigScope } from "./cbxConfig/types.js";
12
12
  import type { VaultManifest } from "./vault/types.js";
13
- import {
14
- // Skill tools
15
- skillListCategoriesName,
16
- skillListCategoriesDescription,
17
- skillListCategoriesSchema,
18
- handleSkillListCategories,
19
- skillBrowseCategoryName,
20
- skillBrowseCategoryDescription,
21
- skillBrowseCategorySchema,
22
- handleSkillBrowseCategory,
23
- skillSearchName,
24
- skillSearchDescription,
25
- skillSearchSchema,
26
- handleSkillSearch,
27
- skillGetName,
28
- skillGetDescription,
29
- skillGetSchema,
30
- handleSkillGet,
31
- skillBudgetReportName,
32
- skillBudgetReportDescription,
33
- skillBudgetReportSchema,
34
- handleSkillBudgetReport,
35
- // Postman tools
36
- postmanGetModeName,
37
- postmanGetModeDescription,
38
- postmanGetModeSchema,
39
- handlePostmanGetMode,
40
- postmanSetModeName,
41
- postmanSetModeDescription,
42
- postmanSetModeSchema,
43
- handlePostmanSetMode,
44
- postmanGetStatusName,
45
- postmanGetStatusDescription,
46
- postmanGetStatusSchema,
47
- handlePostmanGetStatus,
48
- // Stitch tools
49
- stitchGetModeName,
50
- stitchGetModeDescription,
51
- stitchGetModeSchema,
52
- handleStitchGetMode,
53
- stitchSetProfileName,
54
- stitchSetProfileDescription,
55
- stitchSetProfileSchema,
56
- handleStitchSetProfile,
57
- stitchGetStatusName,
58
- stitchGetStatusDescription,
59
- stitchGetStatusSchema,
60
- handleStitchGetStatus,
61
- } from "./tools/index.js";
13
+ import { TOOL_REGISTRY, type ToolRuntimeContext } from "./tools/registry.js";
62
14
  import {
63
15
  callUpstreamTool,
64
16
  discoverUpstreamCatalogs,
65
17
  } from "./upstream/passthrough.js";
18
+ import { logger } from "./utils/logger.js";
66
19
 
67
20
  export interface CreateServerOptions {
68
21
  config: ServerConfig;
@@ -102,137 +55,37 @@ export async function createServer({
102
55
  config,
103
56
  manifest,
104
57
  defaultConfigScope = "auto",
105
- }: CreateServerOptions): McpServer {
58
+ }: CreateServerOptions): Promise<McpServer> {
106
59
  const server = new McpServer({
107
60
  name: config.server.name,
108
61
  version: config.server.version,
109
62
  });
110
63
 
111
- const maxLen = config.vault.summaryMaxLength;
112
- const charsPerToken = config.telemetry?.charsPerToken ?? 4;
113
- const withDefaultScope = (
114
- args: Record<string, unknown> | undefined,
115
- ): Record<string, unknown> => {
116
- const safeArgs = args ?? {};
117
- return {
118
- ...safeArgs,
119
- scope:
120
- typeof safeArgs.scope === "string" ? safeArgs.scope : defaultConfigScope,
121
- };
122
- };
123
-
124
- // ─── Skill vault tools ───────────────────────────────────────
125
-
126
- server.tool(
127
- skillListCategoriesName,
128
- skillListCategoriesDescription,
129
- skillListCategoriesSchema.shape,
130
- async () => handleSkillListCategories(manifest, charsPerToken),
131
- );
132
-
133
- server.tool(
134
- skillBrowseCategoryName,
135
- skillBrowseCategoryDescription,
136
- skillBrowseCategorySchema.shape,
137
- async (args) =>
138
- handleSkillBrowseCategory(args, manifest, maxLen, charsPerToken),
139
- );
140
-
141
- server.tool(
142
- skillSearchName,
143
- skillSearchDescription,
144
- skillSearchSchema.shape,
145
- async (args) => handleSkillSearch(args, manifest, maxLen, charsPerToken),
146
- );
147
-
148
- server.tool(
149
- skillGetName,
150
- skillGetDescription,
151
- skillGetSchema.shape,
152
- async (args) => handleSkillGet(args, manifest, charsPerToken),
153
- );
154
-
155
- server.tool(
156
- skillBudgetReportName,
157
- skillBudgetReportDescription,
158
- skillBudgetReportSchema.shape,
159
- async (args) => handleSkillBudgetReport(args, manifest, charsPerToken),
160
- );
64
+ // ─── Built-in tools from declarative registry ─────────────
161
65
 
162
- // ─── Postman tools ──────────────────────────────────────────
163
-
164
- server.tool(
165
- postmanGetModeName,
166
- postmanGetModeDescription,
167
- postmanGetModeSchema.shape,
168
- async (args) =>
169
- handlePostmanGetMode(
170
- withDefaultScope(args as Record<string, unknown>) as z.infer<
171
- typeof postmanGetModeSchema
172
- >,
173
- ),
174
- );
175
-
176
- server.tool(
177
- postmanSetModeName,
178
- postmanSetModeDescription,
179
- postmanSetModeSchema.shape,
180
- async (args) =>
181
- handlePostmanSetMode(
182
- withDefaultScope(args as Record<string, unknown>) as z.infer<
183
- typeof postmanSetModeSchema
184
- >,
185
- ),
186
- );
187
-
188
- server.tool(
189
- postmanGetStatusName,
190
- postmanGetStatusDescription,
191
- postmanGetStatusSchema.shape,
192
- async (args) =>
193
- handlePostmanGetStatus(
194
- withDefaultScope(args as Record<string, unknown>) as z.infer<
195
- typeof postmanGetStatusSchema
196
- >,
197
- ),
198
- );
199
-
200
- // ─── Stitch tools ──────────────────────────────────────────
201
-
202
- server.tool(
203
- stitchGetModeName,
204
- stitchGetModeDescription,
205
- stitchGetModeSchema.shape,
206
- async (args) =>
207
- handleStitchGetMode(
208
- withDefaultScope(args as Record<string, unknown>) as z.infer<
209
- typeof stitchGetModeSchema
210
- >,
211
- ),
212
- );
66
+ const runtimeCtx: ToolRuntimeContext = {
67
+ manifest,
68
+ charsPerToken: config.telemetry?.charsPerToken ?? 4,
69
+ summaryMaxLength: config.vault.summaryMaxLength,
70
+ defaultConfigScope,
71
+ };
213
72
 
214
- server.tool(
215
- stitchSetProfileName,
216
- stitchSetProfileDescription,
217
- stitchSetProfileSchema.shape,
218
- async (args) =>
219
- handleStitchSetProfile(
220
- withDefaultScope(args as Record<string, unknown>) as z.infer<
221
- typeof stitchSetProfileSchema
222
- >,
223
- ),
224
- );
73
+ for (const entry of TOOL_REGISTRY) {
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(
80
+ entry.name,
81
+ entry.description,
82
+ entry.schema.shape,
83
+ handler,
84
+ );
85
+ }
225
86
 
226
- server.tool(
227
- stitchGetStatusName,
228
- stitchGetStatusDescription,
229
- stitchGetStatusSchema.shape,
230
- async (args) =>
231
- handleStitchGetStatus(
232
- withDefaultScope(args as Record<string, unknown>) as z.infer<
233
- typeof stitchGetStatusSchema
234
- >,
235
- ),
87
+ logger.debug(
88
+ `Registered ${TOOL_REGISTRY.length} built-in tools from registry`,
236
89
  );
237
90
 
238
91
  // ─── Dynamic upstream passthrough tools ────────────────────
@@ -242,21 +95,30 @@ export async function createServer({
242
95
  for (const catalog of [upstreamCatalogs.postman, upstreamCatalogs.stitch]) {
243
96
  for (const tool of catalog.tools) {
244
97
  const namespaced = tool.namespacedName;
245
- server.tool(
98
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
+ (server as any).tool(
246
100
  namespaced,
247
101
  `[${catalog.service} passthrough] ${tool.description || tool.name}`,
248
102
  dynamicArgsShape,
249
- async (args) => {
103
+ async (args: Record<string, unknown>) => {
250
104
  try {
251
105
  const result = await callUpstreamTool({
252
106
  service: catalog.service,
253
107
  name: tool.name,
254
108
  argumentsValue:
255
- args && typeof args === "object" ? args : ({} as Record<string, unknown>),
109
+ args && typeof args === "object"
110
+ ? args
111
+ : ({} as Record<string, unknown>),
256
112
  });
257
113
  return {
258
- content: result.content ?? [],
259
- structuredContent: result.structuredContent,
114
+ // SDK content is typed broadly; cast to the expected array shape.
115
+ content: (result.content ?? []) as Array<{
116
+ type: string;
117
+ [k: string]: unknown;
118
+ }>,
119
+ structuredContent: result.structuredContent as
120
+ | Record<string, unknown>
121
+ | undefined,
260
122
  isError: Boolean(result.isError),
261
123
  };
262
124
  } catch (error) {
@@ -16,6 +16,6 @@ When adding a new tool:
16
16
 
17
17
  1. Create `toolName.ts` in `../` (parent `tools/` directory)
18
18
  2. Export name, description, schema, and handler
19
- 3. Register in `../index.ts` barrel export
20
- 4. Register in `../../server.ts` with `mcpServer.tool()`
21
- 5. Add tests in `../../__tests__/tools/`
19
+ 3. Add a `ToolRegistryEntry` in `../registry.ts` (this auto-registers the tool)
20
+ 4. Add tests in a `toolName.test.ts` file
21
+ 5. Run `npm run generate:mcp-manifest` to update the build-time manifest
@@ -3,8 +3,22 @@
3
3
  *
4
4
  * Central registry exporting all tool metadata and handlers.
5
5
  * Used by server.ts to register tools with McpServer.
6
+ *
7
+ * The declarative registry (registry.ts) is the preferred way to register
8
+ * new tools. Individual tool exports are kept for backward compatibility
9
+ * and direct unit testing.
6
10
  */
7
11
 
12
+ export {
13
+ TOOL_REGISTRY,
14
+ getToolsByCategory,
15
+ getRegisteredToolNames,
16
+ buildRegistrySummary,
17
+ type ToolCategory,
18
+ type ToolRegistryEntry,
19
+ type ToolRuntimeContext,
20
+ } from "./registry.js";
21
+
8
22
  export {
9
23
  skillListCategoriesName,
10
24
  skillListCategoriesDescription,
@@ -0,0 +1,121 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ TOOL_REGISTRY,
4
+ getToolsByCategory,
5
+ getRegisteredToolNames,
6
+ buildRegistrySummary,
7
+ type ToolRegistryEntry,
8
+ type ToolRuntimeContext,
9
+ } from "./registry.js";
10
+ import type { VaultManifest } from "../vault/types.js";
11
+
12
+ function createTestContext(): ToolRuntimeContext {
13
+ const manifest: VaultManifest = {
14
+ categories: ["frontend", "backend"],
15
+ skills: [
16
+ {
17
+ id: "react-expert",
18
+ category: "frontend",
19
+ path: "/tmp/react-expert/SKILL.md",
20
+ fileBytes: 100,
21
+ },
22
+ ],
23
+ fullCatalogBytes: 100,
24
+ fullCatalogEstimatedTokens: 25,
25
+ };
26
+
27
+ return {
28
+ manifest,
29
+ charsPerToken: 4,
30
+ summaryMaxLength: 200,
31
+ defaultConfigScope: "auto",
32
+ };
33
+ }
34
+
35
+ describe("tool registry", () => {
36
+ it("contains all expected built-in tools", () => {
37
+ const names = getRegisteredToolNames();
38
+ expect(names).toContain("skill_list_categories");
39
+ expect(names).toContain("skill_browse_category");
40
+ expect(names).toContain("skill_search");
41
+ expect(names).toContain("skill_get");
42
+ expect(names).toContain("skill_budget_report");
43
+ expect(names).toContain("postman_get_mode");
44
+ expect(names).toContain("postman_set_mode");
45
+ expect(names).toContain("postman_get_status");
46
+ expect(names).toContain("stitch_get_mode");
47
+ expect(names).toContain("stitch_set_profile");
48
+ expect(names).toContain("stitch_get_status");
49
+ });
50
+
51
+ it("has exactly 11 built-in tools", () => {
52
+ expect(TOOL_REGISTRY).toHaveLength(11);
53
+ });
54
+
55
+ it("has no duplicate tool names", () => {
56
+ const names = getRegisteredToolNames();
57
+ const unique = new Set(names);
58
+ expect(unique.size).toBe(names.length);
59
+ });
60
+
61
+ it("every entry has required fields", () => {
62
+ for (const entry of TOOL_REGISTRY) {
63
+ expect(entry.name).toBeTruthy();
64
+ expect(entry.description).toBeTruthy();
65
+ expect(entry.schema).toBeTruthy();
66
+ expect(entry.category).toMatch(/^(skill|postman|stitch)$/);
67
+ expect(typeof entry.createHandler).toBe("function");
68
+ }
69
+ });
70
+
71
+ it("filters by category", () => {
72
+ const skillTools = getToolsByCategory("skill");
73
+ expect(skillTools).toHaveLength(5);
74
+ expect(skillTools.every((t) => t.category === "skill")).toBe(true);
75
+
76
+ const postmanTools = getToolsByCategory("postman");
77
+ expect(postmanTools).toHaveLength(3);
78
+
79
+ const stitchTools = getToolsByCategory("stitch");
80
+ expect(stitchTools).toHaveLength(3);
81
+ });
82
+
83
+ it("creates handlers from runtime context", () => {
84
+ const ctx = createTestContext();
85
+ for (const entry of TOOL_REGISTRY) {
86
+ const handler = entry.createHandler(ctx);
87
+ expect(typeof handler).toBe("function");
88
+ }
89
+ });
90
+
91
+ it("skill_list_categories handler returns valid result from registry", async () => {
92
+ const ctx = createTestContext();
93
+ const entry = TOOL_REGISTRY.find(
94
+ (t) => t.name === "skill_list_categories",
95
+ )!;
96
+ const handler = entry.createHandler(ctx);
97
+ const result = (await handler({})) as {
98
+ content: Array<{ text: string }>;
99
+ };
100
+ const data = JSON.parse(result.content[0].text);
101
+ expect(data.totalSkills).toBe(1);
102
+ });
103
+
104
+ it("buildRegistrySummary produces correct structure", () => {
105
+ const summary = buildRegistrySummary();
106
+ expect(summary.totalTools).toBe(11);
107
+ expect(summary.categories).toHaveProperty("skill");
108
+ expect(summary.categories).toHaveProperty("postman");
109
+ expect(summary.categories).toHaveProperty("stitch");
110
+ expect(summary.categories.skill.tools).toHaveLength(5);
111
+ expect(summary.categories.postman.tools).toHaveLength(3);
112
+ expect(summary.categories.stitch.tools).toHaveLength(3);
113
+ });
114
+
115
+ it("each schema has a valid .shape property", () => {
116
+ for (const entry of TOOL_REGISTRY) {
117
+ expect(entry.schema.shape).toBeDefined();
118
+ expect(typeof entry.schema.shape).toBe("object");
119
+ }
120
+ });
121
+ });