Skip to content

Commit 364a08d

Browse files
committed
feat: implement tool name sanitization and enhance tool tag processing functionality
1 parent 6ccdde7 commit 364a08d

File tree

7 files changed

+114
-10
lines changed

7 files changed

+114
-10
lines changed

packages/core/src/plugins/built-in/tool-name-mapping-plugin.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,35 @@ import type { ToolPlugin, TransformContext } from "../../plugin-types.ts";
33
/**
44
* Built-in plugin that handles tool name mapping between dot and underscore notation
55
* Allows tools to be referenced with both formats (e.g., "server.tool" and "server_tool")
6+
* Also sanitizes tool names to comply with API restrictions
67
*/
78
export const createToolNameMappingPlugin = (): ToolPlugin => ({
89
name: "built-in-tool-name-mapping",
910
version: "1.0.0",
1011
enforce: "pre", // Apply early to establish mappings
1112
transformTool: (tool, context: TransformContext) => {
1213
const server = context.server;
13-
const toolName = context.toolName;
14+
const toolName = context.toolName; // Sanitized name (e.g., "_c_desktop-commander_start_process")
15+
16+
// Get original name if available (e.g., "@c/desktop-commander.start_process")
17+
const originalName = (tool as any)._originalName || toolName;
1418

1519
// Create bidirectional mapping between dot and underscore notation
16-
const dotNotation = toolName.replace(/_/g, ".");
17-
const underscoreNotation = toolName.replace(/\./g, "_");
20+
// Based on ORIGINAL name to support both @scope/server.tool and @scope/server_tool
21+
const dotNotation = originalName.replace(/_/g, ".");
22+
const underscoreNotation = originalName.replace(/\./g, "_");
1823

19-
if (dotNotation !== toolName && server.toolNameMapping) {
24+
if (dotNotation !== originalName && server.toolNameMapping) {
2025
server.toolNameMapping.set(dotNotation, toolName);
21-
server.toolNameMapping.set(toolName, dotNotation);
2226
}
2327

24-
if (underscoreNotation !== toolName && server.toolNameMapping) {
28+
if (underscoreNotation !== originalName && server.toolNameMapping) {
2529
server.toolNameMapping.set(underscoreNotation, toolName);
26-
server.toolNameMapping.set(toolName, underscoreNotation);
30+
}
31+
32+
// Also map the original name to sanitized name
33+
if (originalName !== toolName && server.toolNameMapping) {
34+
server.toolNameMapping.set(originalName, toolName);
2735
}
2836

2937
return tool;

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
} from "../../service/tools.ts";
99
import type z from "zod";
1010
import { smitheryToolNameCompatibale } from "./registory.ts";
11+
import { sanitizePropertyKey } from "./provider.ts";
1112
import { cwd } from "node:process";
1213
import process from "node:process";
1314
import { createHash } from "node:crypto";
@@ -179,7 +180,9 @@ export async function composeMcpDepTools(
179180
tools.forEach((tool) => {
180181
const { toolNameWithScope, toolName: internalToolName } =
181182
smitheryToolNameCompatibale(tool.name, name);
182-
const toolId = `${serverId}_${internalToolName}`;
183+
// Sanitize toolId to ensure it only contains valid characters
184+
const rawToolId = `${serverId}_${internalToolName}`;
185+
const toolId = sanitizePropertyKey(rawToolId);
183186
if (
184187
filterIn &&
185188
!filterIn({
@@ -206,7 +209,12 @@ export async function composeMcpDepTools(
206209
},
207210
);
208211

209-
allTools[toolId] = { ...tool, execute };
212+
// Store the original toolNameWithScope for mapping purposes
213+
allTools[toolId] = {
214+
...tool,
215+
execute,
216+
_originalName: toolNameWithScope,
217+
};
210218
});
211219
} catch (error) {
212220
console.error(`Error creating MCP client for ${name}:`, error);

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ import { optionalObject } from "./json.ts";
1010
*/
1111
export const ToolNameRegex = /^[a-zA-Z0-9_-]{1,64}$/;
1212

13+
/**
14+
* Sanitize tool name to match provider requirements
15+
* Replaces any character that's not alphanumeric, underscore, or dash with underscore
16+
* Truncates to 64 characters max
17+
*/
18+
export function sanitizePropertyKey(name: string): string {
19+
return name
20+
.replace(/[^a-zA-Z0-9_-]/g, "_") // Replace invalid characters with underscore
21+
.substring(0, 64); // Truncate to max length
22+
}
23+
1324
/**
1425
* Conditionally adds additionalProperties to schema based on provider support
1526
*

packages/core/src/utils/common/tool-tag-processor.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ export function processToolTags({
7171
if (toolId) {
7272
// Replace <tool> with <action action="..."/> in the DOM
7373
$(toolEl).replaceWith(`<action action="${toolId}"/>`);
74+
} else {
75+
// Tool not found, remove the tag completely
76+
$(toolEl).remove();
7477
}
7578
}
7679
});

packages/core/src/utils/compose-helpers.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
99
import type { JSONSchema } from "../types.ts";
1010
import { updateRefPaths } from "./common/schema.ts";
1111
import { jsonSchema } from "./schema.ts";
12+
import { sanitizePropertyKey } from "./common/provider.ts";
1213

1314
/**
1415
* Process tools with plugin transformations
@@ -123,7 +124,10 @@ export function buildDependencyGroups(
123124

124125
const updatedProperties = updateRefPaths(baseProperties, toolName);
125126

126-
depGroups[toolName] = {
127+
// Sanitize tool name for use as schema property key
128+
const sanitizedKey = sanitizePropertyKey(toolName);
129+
130+
depGroups[sanitizedKey] = {
127131
type: "object",
128132
description: tool.description,
129133
properties: updatedProperties,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Test tool name sanitization - core functionality
3+
*
4+
* Verifies that sanitizePropertyKey properly handles special characters.
5+
*/
6+
7+
import { assertEquals } from "@std/assert";
8+
import { sanitizePropertyKey } from "../../src/utils/common/provider.ts";
9+
10+
Deno.test("sanitizePropertyKey removes special characters", () => {
11+
// Test real-world MCP tool name: @c/desktop-commander.start_process
12+
assertEquals(
13+
sanitizePropertyKey("@c/desktop-commander.start_process"),
14+
"_c_desktop-commander_start_process",
15+
);
16+
17+
// Verify dash is preserved
18+
assertEquals(
19+
sanitizePropertyKey("server-tool-name"),
20+
"server-tool-name",
21+
);
22+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Test tool tag processing - core functionality
3+
*
4+
* Verifies that processToolTags properly handles tool tags.
5+
*/
6+
7+
import { assertEquals } from "@std/assert";
8+
import { processToolTags } from "../../src/utils/common/tool-tag-processor.ts";
9+
import { load } from "cheerio";
10+
11+
Deno.test("processToolTags replaces existing tools and removes missing ones", () => {
12+
const description =
13+
'Use <tool name="existing"/> and <tool name="missing"/> tools';
14+
const $ = load(description);
15+
const tagToResults = { tool: $("tool").toArray() };
16+
17+
const tools = {
18+
"existing": {
19+
name: "existing",
20+
description: "Existing tool",
21+
inputSchema: { type: "object" as const },
22+
execute: () => {},
23+
},
24+
} as any;
25+
26+
const result = processToolTags({
27+
description,
28+
tagToResults,
29+
$,
30+
tools,
31+
toolOverrides: new Map(),
32+
toolNameMapping: new Map([["existing", "existing"]]),
33+
});
34+
35+
// Should replace existing tool
36+
assertEquals(
37+
result.includes('action="existing"'),
38+
true,
39+
"Should replace existing tool with action tag",
40+
);
41+
42+
// Should remove missing tool tag
43+
assertEquals(
44+
result.includes('name="missing"'),
45+
false,
46+
"Should remove missing tool tag",
47+
);
48+
});

0 commit comments

Comments
 (0)