Skip to content

Commit d051a5e

Browse files
nzhuclaude
andcommitted
feat: add tool name aliasing for Bedrock compatibility
Some LLM providers (e.g., AWS Bedrock) enforce a 64-character limit on tool names. When MCP clients add prefixes like `mcp__plugin_<pkg>_<server>__`, the full tool name can exceed this limit. Add a `--max-tool-name-length` CLI option that enables deterministic, collision-safe shortening of tool names using human-readable abbreviations. Internal handler dispatch, logging, and telemetry continue using the original names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e6b7a09 commit d051a5e

4 files changed

Lines changed: 500 additions & 1 deletion

File tree

src/bin/chrome-devtools-mcp-cli-options.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,11 @@ export const cliOptions = {
253253
'Set by Chrome DevTools CLI if the MCP server is started via the CLI client (this arg exists for usage stats)',
254254
hidden: true,
255255
},
256+
maxToolNameLength: {
257+
type: 'number',
258+
describe:
259+
'Maximum length for exported MCP tool names. Names exceeding this limit are automatically shortened with human-readable aliases. Useful when MCP client prefixes cause tool names to exceed provider limits (e.g., AWS Bedrock 64-char limit).',
260+
},
256261
} satisfies Record<string, YargsOptions>;
257262

258263
export type ParsedArguments = ReturnType<typeof parseArguments>;

src/index.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
SetLevelRequestSchema,
2424
} from './third_party/index.js';
2525
import {ToolCategory} from './tools/categories.js';
26+
import {ToolNameAliaser} from './tools/tool-name-aliaser.js';
2627
import type {DefinedPageTool, ToolDefinition} from './tools/ToolDefinition.js';
2728
import {pageIdSchema} from './tools/ToolDefinition.js';
2829
import {createTools} from './tools/tools.js';
@@ -108,6 +109,10 @@ export async function createMcpServer(
108109

109110
const toolMutex = new Mutex();
110111

112+
const aliaser = serverArgs.maxToolNameLength
113+
? new ToolNameAliaser(serverArgs.maxToolNameLength)
114+
: undefined;
115+
111116
function registerTool(tool: ToolDefinition | DefinedPageTool): void {
112117
if (
113118
tool.annotations.category === ToolCategory.EMULATION &&
@@ -159,8 +164,17 @@ export async function createMcpServer(
159164
? {...tool.schema, ...pageIdSchema}
160165
: tool.schema;
161166

167+
const registrationName = aliaser
168+
? aliaser.register(tool.name)
169+
: tool.name;
170+
if (registrationName !== tool.name) {
171+
logger(
172+
`Tool "${tool.name}" aliased to "${registrationName}" (max length: ${serverArgs.maxToolNameLength})`,
173+
);
174+
}
175+
162176
server.registerTool(
163-
tool.name,
177+
registrationName,
164178
{
165179
description: tool.description,
166180
inputSchema: schema,

src/tools/tool-name-aliaser.ts

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/**
8+
* Common abbreviations for tool name segments.
9+
* Used to produce human-readable short aliases.
10+
*/
11+
const ABBREVIATIONS: Record<string, string> = {
12+
action: 'act',
13+
analyze: 'anlz',
14+
console: 'cons',
15+
evaluate: 'eval',
16+
experimental: 'exp',
17+
extension: 'ext',
18+
extensions: 'exts',
19+
insight: 'ins',
20+
install: 'inst',
21+
lighthouse: 'lh',
22+
memory: 'mem',
23+
message: 'msg',
24+
messages: 'msgs',
25+
navigate: 'nav',
26+
network: 'net',
27+
performance: 'perf',
28+
request: 'req',
29+
requests: 'reqs',
30+
screencast: 'scrcast',
31+
screenshot: 'scrn',
32+
snapshot: 'snap',
33+
trigger: 'trig',
34+
uninstall: 'uninst',
35+
};
36+
37+
/**
38+
* Tool name aliaser for provider compatibility.
39+
*
40+
* Some LLM providers (e.g., AWS Bedrock) enforce character limits on tool
41+
* names. When MCP clients add prefixes like
42+
* `mcp__plugin_<pkg>_<server>__`, the full tool name can exceed these limits.
43+
*
44+
* This class provides deterministic, collision-safe shortening of tool names
45+
* while maintaining bidirectional mappings for dispatch.
46+
*
47+
* Example: with a Bedrock 64-char limit and a 49-char client prefix,
48+
* `maxLength` should be set to 15 (64 - 49). Tool names longer than 15
49+
* characters are automatically shortened using human-readable abbreviations.
50+
*/
51+
export class ToolNameAliaser {
52+
readonly #aliasToOriginal = new Map<string, string>();
53+
readonly #originalToAlias = new Map<string, string>();
54+
readonly #maxLength: number;
55+
56+
constructor(maxLength: number) {
57+
if (maxLength < 1) {
58+
throw new Error('maxLength must be at least 1');
59+
}
60+
this.#maxLength = maxLength;
61+
}
62+
63+
get maxLength(): number {
64+
return this.#maxLength;
65+
}
66+
67+
/**
68+
* Register a tool name. Returns the alias (which equals the original name
69+
* if it already fits within the max length).
70+
*
71+
* Tool names should be registered in a deterministic order (e.g.,
72+
* alphabetical) to ensure stable alias generation across runs.
73+
*/
74+
register(originalName: string): string {
75+
if (this.#originalToAlias.has(originalName)) {
76+
return this.#originalToAlias.get(originalName)!;
77+
}
78+
79+
if (originalName.length <= this.#maxLength) {
80+
this.#aliasToOriginal.set(originalName, originalName);
81+
this.#originalToAlias.set(originalName, originalName);
82+
return originalName;
83+
}
84+
85+
const alias = this.#shorten(originalName);
86+
this.#aliasToOriginal.set(alias, originalName);
87+
this.#originalToAlias.set(originalName, alias);
88+
return alias;
89+
}
90+
91+
/**
92+
* Resolve an alias back to its original tool name.
93+
* Returns `undefined` if the alias is not registered.
94+
*/
95+
resolve(alias: string): string | undefined {
96+
return this.#aliasToOriginal.get(alias);
97+
}
98+
99+
/**
100+
* Get the alias for an original tool name.
101+
* Returns `undefined` if the name is not registered.
102+
*/
103+
getAlias(originalName: string): string | undefined {
104+
return this.#originalToAlias.get(originalName);
105+
}
106+
107+
/**
108+
* Get all registered (alias, original) pairs.
109+
*/
110+
entries(): Array<[alias: string, original: string]> {
111+
return [...this.#aliasToOriginal.entries()];
112+
}
113+
114+
#shorten(name: string): string {
115+
const segments = name.split('_');
116+
117+
// Step 1: Apply known abbreviations to each segment.
118+
const abbreviated = segments.map(seg => ABBREVIATIONS[seg] ?? seg);
119+
120+
let candidate = abbreviated.join('_');
121+
if (candidate.length <= this.#maxLength) {
122+
return this.#ensureUnique(candidate);
123+
}
124+
125+
// Step 2: Progressively truncate the longest segment by one character
126+
// until the name fits.
127+
const working = [...abbreviated];
128+
while (working.join('_').length > this.#maxLength && working.length > 0) {
129+
let longestIdx = 0;
130+
for (let i = 1; i < working.length; i++) {
131+
if (working[i].length > working[longestIdx].length) {
132+
longestIdx = i;
133+
}
134+
}
135+
if (working[longestIdx].length <= 1) {
136+
// Cannot shorten further; drop the last segment.
137+
working.pop();
138+
continue;
139+
}
140+
working[longestIdx] = working[longestIdx].slice(0, -1);
141+
}
142+
143+
candidate = working.join('_');
144+
145+
// Step 3: Hard truncate as a safety net (shouldn't be reached by the
146+
// loop above for reasonable maxLength values).
147+
if (candidate.length > this.#maxLength) {
148+
candidate = candidate.slice(0, this.#maxLength);
149+
}
150+
151+
return this.#ensureUnique(candidate);
152+
}
153+
154+
#ensureUnique(candidate: string): string {
155+
if (!this.#aliasToOriginal.has(candidate)) {
156+
return candidate;
157+
}
158+
159+
// Collision: append a numeric suffix while staying within maxLength.
160+
for (let i = 1; i < 1000; i++) {
161+
const suffix = `_${i}`;
162+
const maxBase = this.#maxLength - suffix.length;
163+
const base =
164+
candidate.length > maxBase ? candidate.slice(0, maxBase) : candidate;
165+
const withSuffix = base + suffix;
166+
if (!this.#aliasToOriginal.has(withSuffix)) {
167+
return withSuffix;
168+
}
169+
}
170+
171+
throw new Error(
172+
`Cannot generate unique alias for "${candidate}" after 1000 attempts`,
173+
);
174+
}
175+
}

0 commit comments

Comments
 (0)