fix(mcp): shorten tool names to stay under 64 character limit
Claude Desktop enforces a 64 character limit on MCP tool names. - Remove 'tpmjs-tools-' and 'tpmjs-' prefixes from package names - Remove 'Tool' suffix from tool names - Add fallback truncation if still too long - Update parser to try all possible package/tool name combinations
This commit is contained in:
parent
3324034e6f
commit
46b212e65a
2 changed files with 72 additions and 30 deletions
|
|
@ -169,22 +169,19 @@ export async function handleToolsCall(
|
|||
'Database query timed out'
|
||||
);
|
||||
|
||||
// Try both scoped (@scope/name) and literal (name-with-hyphens) package name interpretations
|
||||
// This handles cases like:
|
||||
// - @tpmjs/hello (scoped) → sanitized as tpmjs-hello → parsed back as @tpmjs/hello
|
||||
// - firecrawl-aisdk (not scoped) → sanitized as firecrawl-aisdk → should match as-is
|
||||
let collectionTool = collection?.tools.find(
|
||||
(ct) =>
|
||||
ct.tool.package.npmPackageName === parsed.packageName && ct.tool.name === parsed.toolName
|
||||
);
|
||||
// Try all possible package name and tool name combinations
|
||||
// This handles both old format (full package name) and new shortened format
|
||||
type CollectionTool = NonNullable<typeof collection>['tools'][number];
|
||||
let collectionTool: CollectionTool | undefined;
|
||||
|
||||
// If not found with scoped name, try the literal package name
|
||||
if (!collectionTool && parsed.literalPackageName !== parsed.packageName) {
|
||||
collectionTool = collection?.tools.find(
|
||||
(ct) =>
|
||||
ct.tool.package.npmPackageName === parsed.literalPackageName &&
|
||||
ct.tool.name === parsed.toolName
|
||||
);
|
||||
for (const pkgName of parsed.possiblePackages) {
|
||||
for (const toolName of parsed.possibleToolNames) {
|
||||
collectionTool = collection?.tools.find(
|
||||
(ct) => ct.tool.package.npmPackageName === pkgName && ct.tool.name === toolName
|
||||
);
|
||||
if (collectionTool) break;
|
||||
}
|
||||
if (collectionTool) break;
|
||||
}
|
||||
|
||||
if (!collectionTool) {
|
||||
|
|
|
|||
|
|
@ -19,13 +19,37 @@ export interface BridgeTool {
|
|||
|
||||
/**
|
||||
* Sanitize package name and tool name into a valid MCP tool name.
|
||||
* MCP tool names must match ^[a-zA-Z0-9_-]+
|
||||
* MCP tool names must match ^[a-zA-Z0-9_-]+ and be at most 64 characters.
|
||||
*
|
||||
* Example: @tpmjs/hello + helloWorldTool → tpmjs-hello--helloWorldTool
|
||||
* Example: @tpmjs/tools-sprites-foo + spritesBarTool → sprites-foo--spritesBar
|
||||
*/
|
||||
export function sanitizeMcpName(packageName: string, toolName: string): string {
|
||||
const sanitizedPkg = packageName.replace(/^@/, '').replace(/\//g, '-');
|
||||
return `${sanitizedPkg}--${toolName}`;
|
||||
// Remove @ and convert / to -
|
||||
let sanitizedPkg = packageName.replace(/^@/, '').replace(/\//g, '-');
|
||||
|
||||
// Remove common prefixes to shorten
|
||||
sanitizedPkg = sanitizedPkg.replace(/^tpmjs-tools-/, '');
|
||||
sanitizedPkg = sanitizedPkg.replace(/^tpmjs-/, '');
|
||||
|
||||
// Remove 'Tool' suffix from tool name
|
||||
const sanitizedTool = toolName.replace(/Tool$/, '');
|
||||
|
||||
const fullName = `${sanitizedPkg}--${sanitizedTool}`;
|
||||
|
||||
// Ensure we stay under 64 character limit
|
||||
if (fullName.length <= 64) {
|
||||
return fullName;
|
||||
}
|
||||
|
||||
// If still too long, truncate package name to fit
|
||||
const maxPkgLen = 64 - sanitizedTool.length - 2; // 2 for '--'
|
||||
if (maxPkgLen > 10) {
|
||||
return `${sanitizedPkg.slice(0, maxPkgLen)}--${sanitizedTool}`;
|
||||
}
|
||||
|
||||
// Last resort: truncate both
|
||||
return fullName.slice(0, 64);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -57,7 +81,14 @@ export function convertToMcpTool(tool: Tool & { package: Package }): McpToolDefi
|
|||
* Parsed tool name result - either a registry tool or a bridge tool
|
||||
*/
|
||||
export type ParsedToolName =
|
||||
| { type: 'registry'; packageName: string; literalPackageName: string; toolName: string }
|
||||
| {
|
||||
type: 'registry';
|
||||
packageName: string;
|
||||
literalPackageName: string;
|
||||
toolName: string;
|
||||
possiblePackages: string[];
|
||||
possibleToolNames: string[];
|
||||
}
|
||||
| { type: 'bridge'; serverId: string; toolName: string };
|
||||
|
||||
/**
|
||||
|
|
@ -65,6 +96,7 @@ export type ParsedToolName =
|
|||
* Handles both registry tools and bridge tools.
|
||||
*
|
||||
* Registry: tpmjs-hello--helloWorldTool → { type: 'registry', packageName: "@tpmjs/hello", toolName: "helloWorldTool" }
|
||||
* Shortened: sprites-get--spritesGet → tries @tpmjs/tools-sprites-get, @tpmjs/sprites-get, sprites-get
|
||||
* Bridge: bridge--chrome-devtools--screenshot → { type: 'bridge', serverId: "chrome-devtools", toolName: "screenshot" }
|
||||
*/
|
||||
export function parseToolName(mcpName: string): ParsedToolName | null {
|
||||
|
|
@ -83,21 +115,34 @@ export function parseToolName(mcpName: string): ParsedToolName | null {
|
|||
if (!match || !match[1] || !match[2]) return null;
|
||||
|
||||
const pkg = match[1];
|
||||
const toolName = match[2];
|
||||
let toolName = match[2];
|
||||
|
||||
// Try to reconstruct @scope/name format if it looks scoped
|
||||
// tpmjs-hello → @tpmjs/hello (first dash becomes @scope/)
|
||||
// But also keep the original for non-scoped packages like firecrawl-aisdk
|
||||
const scopedPackageName = pkg.includes('-') ? `@${pkg.replace('-', '/')}` : pkg;
|
||||
const literalPackageName = pkg;
|
||||
// Add back 'Tool' suffix if it was removed (try both with and without)
|
||||
// The handler will try to find the tool with both variants
|
||||
const toolNameWithSuffix = toolName.endsWith('Tool') ? toolName : `${toolName}Tool`;
|
||||
|
||||
// Return both possible interpretations - the handler will try both
|
||||
// Generate possible package names to try:
|
||||
// 1. @tpmjs/tools-{pkg} (shortened tpmjs-tools- prefix)
|
||||
// 2. @tpmjs/{pkg} (shortened tpmjs- prefix)
|
||||
// 3. @{scope}/{name} (standard scoped, first dash becomes /)
|
||||
// 4. {pkg} as literal (non-scoped packages)
|
||||
const possiblePackages = [
|
||||
`@tpmjs/tools-${pkg}`,
|
||||
`@tpmjs/${pkg}`,
|
||||
pkg.includes('-') ? `@${pkg.replace('-', '/')}` : `@${pkg}`,
|
||||
pkg,
|
||||
];
|
||||
|
||||
// Return the first scoped interpretation as primary, with literal as fallback
|
||||
return {
|
||||
type: 'registry',
|
||||
packageName: scopedPackageName,
|
||||
literalPackageName,
|
||||
toolName,
|
||||
};
|
||||
packageName: possiblePackages[0]!, // @tpmjs/tools-{pkg}
|
||||
literalPackageName: pkg,
|
||||
toolName: toolNameWithSuffix,
|
||||
// Additional candidates for the handler to try
|
||||
possiblePackages,
|
||||
possibleToolNames: [toolNameWithSuffix, toolName],
|
||||
} as ParsedToolName;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue