sync: update from private repo (8ee7d54)
Some checks failed
CI / build-and-test (push) Has been cancelled
Some checks failed
CI / build-and-test (push) Has been cancelled
This commit is contained in:
parent
b3747add6b
commit
5c3d7bb5c4
@ -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<string, string> = {
|
||||
'image/png': 'png',
|
||||
@ -62,7 +64,7 @@ export type SaveBinaryResult =
|
||||
|
||||
export async function saveBinary(input: SaveBinaryInput): Promise<SaveBinaryResult> {
|
||||
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}` };
|
||||
|
||||
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
41
src/mcp/tool-cache.test.ts
Normal file
41
src/mcp/tool-cache.test.ts
Normal file
@ -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']);
|
||||
});
|
||||
});
|
||||
@ -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(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user