From 5c3d7bb5c4fc3cb45f23b69e2ff95ace2ddcb070 Mon Sep 17 00:00:00 2001 From: oss-sync Date: Tue, 9 Jun 2026 14:38:42 +0000 Subject: [PATCH] sync: update from private repo (8ee7d54) --- src/mcp/binary-saver.ts | 4 +++- src/mcp/tool-adapter.test.ts | 19 +++++++++++++++++ src/mcp/tool-adapter.ts | 9 ++++++-- src/mcp/tool-cache.test.ts | 41 ++++++++++++++++++++++++++++++++++++ src/mcp/tool-cache.ts | 9 +++++--- 5 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 src/mcp/tool-cache.test.ts diff --git a/src/mcp/binary-saver.ts b/src/mcp/binary-saver.ts index 3e47812..a4cba5b 100644 --- a/src/mcp/binary-saver.ts +++ b/src/mcp/binary-saver.ts @@ -4,6 +4,8 @@ import { randomBytes } from 'node:crypto'; import { logger } from '../logger.js'; const SLUG = /^[a-z0-9_-]{1,64}$/; +// Tool names from remote MCP servers may be camelCase (allow uppercase). +const TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/; const MIME_TO_EXT: Record = { 'image/png': 'png', @@ -62,7 +64,7 @@ export type SaveBinaryResult = export async function saveBinary(input: SaveBinaryInput): Promise { if (!SLUG.test(input.serverId)) return { ok: false, reason: 'invalid serverId slug' }; - if (!SLUG.test(input.toolName)) return { ok: false, reason: 'invalid toolName slug' }; + if (!TOOL_NAME.test(input.toolName)) return { ok: false, reason: 'invalid toolName slug' }; if (input.bytes.length > input.maxBytes) return { ok: false, reason: `binary exceeds max ${input.maxBytes}B` }; if (!magicMatches(input.bytes, input.mimeType)) { return { ok: false, reason: `magic bytes do not match mimeType ${input.mimeType}` }; diff --git a/src/mcp/tool-adapter.test.ts b/src/mcp/tool-adapter.test.ts index f2895eb..554b944 100644 --- a/src/mcp/tool-adapter.test.ts +++ b/src/mcp/tool-adapter.test.ts @@ -32,6 +32,12 @@ describe('normalizeToolName / parseToolName', () => { it('rejects slug violations', () => { expect(parseToolName('mcp__BAD__ok')).toBeNull(); }); + it('accepts camelCase tool names (remote MCP servers, e.g. NocoDB)', () => { + expect(parseToolName('mcp__nocodb__createRecords')).toEqual({ + serverId: 'nocodb', + toolName: 'createRecords', + }); + }); }); describe('buildToolDefsFromCache', () => { @@ -57,4 +63,17 @@ describe('buildToolDefsFromCache', () => { const defs = buildToolDefsFromCache(cache, ['mcp__canva__export_design'], serverNames); expect(defs[0].function.parameters).toEqual({ type: 'object', properties: {} }); }); + it('includes camelCase tool names (does not drop NocoDB-style tools)', () => { + const nocodb = [ + { serverId: 'nocodb', toolName: 'createRecords', description: 'c', inputSchema: null }, + { serverId: 'nocodb', toolName: 'updateRecords', description: 'u', inputSchema: null }, + { serverId: 'nocodb', toolName: 'aggregate', description: 'a', inputSchema: null }, + ]; + const defs = buildToolDefsFromCache(nocodb, ['mcp__nocodb__*'], new Map([['nocodb', 'NocoDB']])); + expect(defs.map((d) => d.function.name)).toEqual([ + 'mcp__nocodb__createRecords', + 'mcp__nocodb__updateRecords', + 'mcp__nocodb__aggregate', + ]); + }); }); diff --git a/src/mcp/tool-adapter.ts b/src/mcp/tool-adapter.ts index fc0a554..6ecf9f1 100644 --- a/src/mcp/tool-adapter.ts +++ b/src/mcp/tool-adapter.ts @@ -1,6 +1,11 @@ import type { ToolDef } from '../llm/openai-compat.js'; +// serverId is our own slug (lowercase). Tool names come from the remote MCP +// server and commonly use camelCase (e.g. NocoDB's `createRecords`), so they +// must allow uppercase — the combined `mcp__id__name` still matches the LLM +// tool-name grammar `[a-zA-Z0-9_-]`. const SLUG = /^[a-z0-9_-]{1,64}$/; +const TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/; export function normalizeToolName(serverId: string, toolName: string): string { return `mcp__${serverId}__${toolName}`; @@ -11,7 +16,7 @@ export function parseToolName(name: string): { serverId: string; toolName: strin const parts = name.split('__'); if (parts.length !== 3) return null; const [, serverId, toolName] = parts; - if (!SLUG.test(serverId) || !SLUG.test(toolName)) return null; + if (!SLUG.test(serverId) || !TOOL_NAME.test(toolName)) return null; return { serverId, toolName }; } @@ -45,7 +50,7 @@ export function buildToolDefsFromCache( const defs: ToolDef[] = []; for (const tool of cache) { - if (!SLUG.test(tool.serverId) || !SLUG.test(tool.toolName)) continue; + if (!SLUG.test(tool.serverId) || !TOOL_NAME.test(tool.toolName)) continue; const canonical = normalizeToolName(tool.serverId, tool.toolName); if (!matchesAnyPattern(canonical, mcpPatterns)) continue; diff --git a/src/mcp/tool-cache.test.ts b/src/mcp/tool-cache.test.ts new file mode 100644 index 0000000..b7fcb17 --- /dev/null +++ b/src/mcp/tool-cache.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import Database from 'better-sqlite3'; +import { runMigrations } from '../db/migrate.js'; +import { createToolCache } from './tool-cache.js'; + +describe('toolCache.replaceForServer', () => { + let db: Database.Database; + afterEach(() => db?.close()); + + function makeDb(): Database.Database { + db = new Database(':memory:'); + db.exec(`CREATE TABLE users (id TEXT PRIMARY KEY);`); + db.exec(`CREATE TABLE jobs (id TEXT PRIMARY KEY, wait_reason TEXT);`); + db.exec(`CREATE TABLE local_tasks (id INTEGER PRIMARY KEY AUTOINCREMENT);`); + runMigrations(db); + return db; + } + + it('keeps camelCase tool names (NocoDB regression: previously only "aggregate" survived)', () => { + const cache = createToolCache(makeDb(), 600); + cache.replaceForServer('nocodb', [ + { name: 'getTablesList' }, + { name: 'queryRecords' }, + { name: 'createRecords' }, + { name: 'updateRecords' }, + { name: 'deleteRecords' }, + { name: 'aggregate' }, + ]); + const names = cache.getForServer('nocodb').map((t) => t.toolName).sort(); + expect(names).toEqual([ + 'aggregate', 'createRecords', 'deleteRecords', 'getTablesList', 'queryRecords', 'updateRecords', + ]); + }); + + it('still skips names with illegal characters', () => { + const cache = createToolCache(makeDb(), 600); + cache.replaceForServer('s', [{ name: 'ok_Tool-1' }, { name: 'bad name!' }, { name: 'has.dot' }]); + const names = cache.getForServer('s').map((t) => t.toolName); + expect(names).toEqual(['ok_Tool-1']); + }); +}); diff --git a/src/mcp/tool-cache.ts b/src/mcp/tool-cache.ts index 9aa68e6..a514e47 100644 --- a/src/mcp/tool-cache.ts +++ b/src/mcp/tool-cache.ts @@ -2,7 +2,10 @@ import type Database from 'better-sqlite3'; import type { CachedTool } from './tool-adapter.js'; import { logger } from '../logger.js'; -const SLUG = /^[a-z0-9_-]{1,64}$/; +// Remote MCP tool names commonly use camelCase (e.g. NocoDB's `createRecords`), +// so allow uppercase. Only the combined `mcp__id__name` must satisfy the LLM +// tool-name grammar, which also permits `[a-zA-Z0-9_-]`. +const TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/; interface Row { server_id: string; @@ -63,8 +66,8 @@ export function createToolCache(db: Database.Database, ttlSeconds: number) { VALUES (?, ?, ?, ?, datetime('now'))`, ); for (const t of tools) { - if (!SLUG.test(t.name)) { - logger.warn(`[mcp:cache] skip tool with invalid slug: ${t.name}`); + if (!TOOL_NAME.test(t.name)) { + logger.warn(`[mcp:cache] skip tool with invalid name: ${t.name}`); continue; } insert.run(