Skip to content

Commit c284110

Browse files
committed
feat: update version to 0.1.1-beta.1; refactor code for improved readability and maintainability; add cleanup functionality for MCP clients
1 parent 2b94a5d commit c284110

File tree

3 files changed

+72
-50
lines changed

3 files changed

+72
-50
lines changed

packages/core/deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mcpc/core",
3-
"version": "0.1.0",
3+
"version": "0.1.1-beta.1",
44
"tasks": {
55
"server:compile": "echo \"no need to compile\"",
66
"test": "deno test --allow-env --allow-read tests/",

packages/core/src/compose.ts

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
ListToolsRequestSchema,
66
type Tool,
77
} from "@modelcontextprotocol/sdk/types.js";
8-
import { jsonSchema, type Schema } from "ai";
8+
import type { Schema } from "ai";
99
import type { McpSettingsSchema } from "./service/tools.ts";
1010
import {
1111
Server,
@@ -19,6 +19,10 @@ import type { JSONSchema, ToolCallback } from "./types.ts";
1919
import { registerAgenticTool } from "./workflow/agentic-tool-registrar.ts";
2020
import { registerAgenticWorkflowTool } from "./workflow/workflow-tool-registrar.ts";
2121

22+
interface ComposedTool extends Tool {
23+
execute: ToolCallback;
24+
}
25+
2226
const ALL_TOOLS_PLACEHOLDER = "__ALL__";
2327
const ACTION_KEY = "action";
2428

@@ -80,7 +84,7 @@ export class ComposableMCPServer extends Server {
8084
description: string,
8185
paramsSchema: Schema<T>,
8286
cb: (args: T, extra?: unknown) => unknown,
83-
internal: boolean = false,
87+
internal: boolean = false
8488
) {
8589
if (!internal) {
8690
const newTool: Tool = {
@@ -144,7 +148,8 @@ export class ComposableMCPServer extends Server {
144148
throw new Error(`Tool ${name} not found`);
145149
}
146150

147-
const callback = this.nameToCb.get(resolvedName) ||
151+
const callback =
152+
this.nameToCb.get(resolvedName) ||
148153
this.internalTools.get(resolvedName)?.callback ||
149154
this.hiddenTools.get(resolvedName) ||
150155
this.composedTools.get(resolvedName);
@@ -154,8 +159,8 @@ export class ComposableMCPServer extends Server {
154159
}
155160

156161
// Apply args transformation if override exists
157-
const override = this.toolOverrides.get(name) ||
158-
this.toolOverrides.get(resolvedName);
162+
const override =
163+
this.toolOverrides.get(name) || this.toolOverrides.get(resolvedName);
159164
const processedArgs = override?.args ? override.args(args) : args;
160165

161166
return await callback(processedArgs);
@@ -166,7 +171,7 @@ export class ComposableMCPServer extends Server {
166171
*/
167172
callInternalTool(name: string, args: unknown): Promise<unknown> {
168173
console.warn(
169-
`callInternalTool() is deprecated. Use callTool() instead for: ${name}`,
174+
`callInternalTool() is deprecated. Use callTool() instead for: ${name}`
170175
);
171176
return this.callTool(name, args);
172177
}
@@ -194,7 +199,7 @@ export class ComposableMCPServer extends Server {
194199
* Get internal tool schema by name
195200
*/
196201
getInternalToolSchema(
197-
name: string,
202+
name: string
198203
): { description: string; schema: JSONSchema } | undefined {
199204
const internalTool = this.internalTools.get(name);
200205
if (internalTool) {
@@ -221,7 +226,7 @@ export class ComposableMCPServer extends Server {
221226
name: string,
222227
description: string,
223228
depsConfig: z.infer<typeof McpSettingsSchema> = { mcpServers: {} },
224-
options: ComposeDefinition["options"] = { mode: "agentic" },
229+
options: ComposeDefinition["options"] = { mode: "agentic" }
225230
) {
226231
const { tagToResults, $ } = parseTags(description, ["tool", "fn"]);
227232

@@ -239,8 +244,8 @@ export class ComposableMCPServer extends Server {
239244
}
240245
});
241246

242-
// Filter tools and transform scoped tool names to valid action identifierss
243-
const tools = await composeMcpDepTools(
247+
// Filter tools and transform scoped tool names to valid action identifiers
248+
const { tools, cleanupClients } = await composeMcpDepTools(
244249
depsConfig,
245250
({ mcpName, toolNameWithScope, toolId }) => {
246251
const matchingStep = options.steps?.find((step) =>
@@ -261,7 +266,7 @@ export class ComposableMCPServer extends Server {
261266

262267
description = description.replace(
263268
$(tool).prop("outerHTML")!,
264-
`<action ${ACTION_KEY}="${toolId}"/>`,
269+
`<action ${ACTION_KEY}="${toolId}"/>`
265270
);
266271
if (selectAll) {
267272
return true;
@@ -271,8 +276,21 @@ export class ComposableMCPServer extends Server {
271276
tool.attribs.name === toolId
272277
);
273278
});
274-
},
275-
);
279+
}
280+
) as { tools: Record<string, ComposedTool>; cleanupClients: () => Promise<void> };
281+
282+
// Cleanup clients when server is closed
283+
this.onclose = async () => {
284+
await cleanupClients();
285+
console.log(`[${name}] MCP server closed, cleaned up dependent clients.`);
286+
};
287+
this.onerror = async (error) => {
288+
console.error(`[${name}] MCP server error:`, error);
289+
await cleanupClients();
290+
console.log(
291+
`[${name}] MCP server error handled, cleaned up dependent clients.`
292+
);
293+
};
276294

277295
// Apply tool overrides
278296
Object.entries(tools).forEach(([toolId, tool]) => {
@@ -281,12 +299,10 @@ export class ComposableMCPServer extends Server {
281299

282300
// If no direct override found, check if we have an override for the dot notation equivalent
283301
if (!override) {
284-
for (
285-
const [
286-
overrideName,
287-
overrideOptions,
288-
] of this.toolOverrides.entries()
289-
) {
302+
for (const [
303+
overrideName,
304+
overrideOptions,
305+
] of this.toolOverrides.entries()) {
290306
// Build the mapping during processing
291307
const dotNotationId = toolId.replace(/_/g, ".");
292308
const underscoreNotationId = overrideName.replace(/\./g, "_");
@@ -333,22 +349,18 @@ export class ComposableMCPServer extends Server {
333349
// For agentic interface: external tools (non-hidden) + internal tools
334350
const allToolNames = [...externalToolNames, ...internalToolNames];
335351
console.log(
336-
`[${name}][composed tools] external: ${
337-
externalToolNames.join(
338-
", ",
339-
)
340-
} | internal: ${internalToolNames.join(", ")}`,
352+
`[${name}][composed tools] external: ${externalToolNames.join(
353+
", "
354+
)} | internal: ${internalToolNames.join(", ")}`
341355
);
342356

343357
const depGroups: Record<string, unknown> = {};
344358
toolNameToDetailList.forEach(([toolName, tool]) => {
345359
if (!tool) {
346360
throw new Error(
347-
`Action ${toolName} not found, available action list: ${
348-
allToolNames.join(
349-
", ",
350-
)
351-
}`,
361+
`Action ${toolName} not found, available action list: ${allToolNames.join(
362+
", "
363+
)}`
352364
);
353365
}
354366

@@ -362,9 +374,10 @@ export class ComposableMCPServer extends Server {
362374
baseSchema.type === "object" && baseSchema.properties
363375
? baseSchema.properties
364376
: {};
365-
const baseRequired = baseSchema.type === "object" && baseSchema.required
366-
? baseSchema.required
367-
: [];
377+
const baseRequired =
378+
baseSchema.type === "object" && Array.isArray(baseSchema.required)
379+
? baseSchema.required
380+
: [];
368381

369382
const updatedProperties = updateRefPaths(baseProperties, toolName);
370383

packages/core/src/utils/common/ai.ts

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ import { cwd } from "node:process";
1515
* Helper type to extract variable names (inside {}) from a template string literal.
1616
* e.g., ExtractVariables<"Hello {name}! You are {age}."> -> "name" | "age"
1717
*/
18-
type ExtractVariables<S extends string> = S extends
19-
`${string}{${infer Var}}${infer Rest}` ? Var extends `${infer ActualVar}}` // Handle potential extra '}' if no Rest or adjacent braces
20-
? ActualVar | ExtractVariables<Rest>
21-
: Var | ExtractVariables<Rest> // Standard case {var}
22-
: never;
18+
type ExtractVariables<S extends string> =
19+
S extends `${string}{${infer Var}}${infer Rest}`
20+
? Var extends `${infer ActualVar}}` // Handle potential extra '}' if no Rest or adjacent braces
21+
? ActualVar | ExtractVariables<Rest>
22+
: Var | ExtractVariables<Rest> // Standard case {var}
23+
: never;
2324

2425
/**
2526
* Type for the input object required by the formatting function.
@@ -51,8 +52,8 @@ interface NativePromptOptions {
5152
*/
5253
export const p = <T extends string>(
5354
template: T,
54-
options: NativePromptOptions = {},
55-
): (input: PromptInput<T>) => string => {
55+
options: NativePromptOptions = {}
56+
): ((input: PromptInput<T>) => string) => {
5657
const { missingVariableHandling = "warn" } = options;
5758

5859
// Pre-compute variable names (at runtime) for the formatting function closure
@@ -64,7 +65,7 @@ export const p = <T extends string>(
6465
variableNames.add(match[1]);
6566
}
6667
const requiredVariables = Array.from(
67-
variableNames,
68+
variableNames
6869
) as (keyof PromptInput<T>)[]; // Runtime list
6970

7071
// Return the formatting function
@@ -85,11 +86,9 @@ export const p = <T extends string>(
8586
switch (missingVariableHandling) {
8687
case "error": {
8788
throw new Error(
88-
`Missing variable "${
89-
String(
90-
variableName,
91-
)
92-
}" in input for template.`,
89+
`Missing variable "${String(
90+
variableName
91+
)}" in input for template.`
9392
);
9493
}
9594
case "warn": {
@@ -105,7 +104,7 @@ export const p = <T extends string>(
105104
case "empty": {
106105
const replaceRegex = new RegExp(
107106
`\\{${String(variableName)}\\}`,
108-
"g",
107+
"g"
109108
);
110109
result = result.replace(replaceRegex, "");
111110
break;
@@ -123,7 +122,7 @@ export const p = <T extends string>(
123122
};
124123
export function parseTags(
125124
htmlString: string,
126-
tags: Array<string>,
125+
tags: Array<string>
127126
): { tagToResults: Record<string, any[]>; $: CheerioAPI } {
128127
const $ = load(htmlString, { xml: { decodeEntities: false } });
129128

@@ -147,9 +146,10 @@ export async function composeMcpDepTools(
147146
toolNameWithScope: string;
148147
internalToolName: string;
149148
toolId: string;
150-
}) => boolean,
149+
}) => boolean
151150
): Promise<Record<string, any>> {
152151
const allTools: Record<string, any> = {};
152+
const allClients: Record<string, Client> = {};
153153

154154
// Process each MCP definition sequentially
155155
for (const [name, definition] of Object.entries(mcpConfig.mcpServers)) {
@@ -188,6 +188,7 @@ export async function composeMcpDepTools(
188188
try {
189189
// Create the MCP client
190190
await client.connect(transport, { timeout: 60_000 * 10 });
191+
allClients[serverId] = client;
191192

192193
// Get the tools from the client
193194
const { tools } = await client.listTools();
@@ -221,5 +222,13 @@ export async function composeMcpDepTools(
221222
}
222223
}
223224

224-
return allTools;
225+
const cleanupClients = async () => {
226+
await Promise.all(
227+
Object.values(allClients).map(async (client) => {
228+
await client.close();
229+
})
230+
);
231+
};
232+
233+
return { tools: allTools, clients: allClients, cleanupClients };
225234
}

0 commit comments

Comments
 (0)