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';
|
import { logger } from '../logger.js';
|
||||||
|
|
||||||
const SLUG = /^[a-z0-9_-]{1,64}$/;
|
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> = {
|
const MIME_TO_EXT: Record<string, string> = {
|
||||||
'image/png': 'png',
|
'image/png': 'png',
|
||||||
@ -62,7 +64,7 @@ export type SaveBinaryResult =
|
|||||||
|
|
||||||
export async function saveBinary(input: SaveBinaryInput): Promise<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.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 (input.bytes.length > input.maxBytes) return { ok: false, reason: `binary exceeds max ${input.maxBytes}B` };
|
||||||
if (!magicMatches(input.bytes, input.mimeType)) {
|
if (!magicMatches(input.bytes, input.mimeType)) {
|
||||||
return { ok: false, reason: `magic bytes do not match mimeType ${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', () => {
|
it('rejects slug violations', () => {
|
||||||
expect(parseToolName('mcp__BAD__ok')).toBeNull();
|
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', () => {
|
describe('buildToolDefsFromCache', () => {
|
||||||
@ -57,4 +63,17 @@ describe('buildToolDefsFromCache', () => {
|
|||||||
const defs = buildToolDefsFromCache(cache, ['mcp__canva__export_design'], serverNames);
|
const defs = buildToolDefsFromCache(cache, ['mcp__canva__export_design'], serverNames);
|
||||||
expect(defs[0].function.parameters).toEqual({ type: 'object', properties: {} });
|
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';
|
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 SLUG = /^[a-z0-9_-]{1,64}$/;
|
||||||
|
const TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/;
|
||||||
|
|
||||||
export function normalizeToolName(serverId: string, toolName: string): string {
|
export function normalizeToolName(serverId: string, toolName: string): string {
|
||||||
return `mcp__${serverId}__${toolName}`;
|
return `mcp__${serverId}__${toolName}`;
|
||||||
@ -11,7 +16,7 @@ export function parseToolName(name: string): { serverId: string; toolName: strin
|
|||||||
const parts = name.split('__');
|
const parts = name.split('__');
|
||||||
if (parts.length !== 3) return null;
|
if (parts.length !== 3) return null;
|
||||||
const [, serverId, toolName] = parts;
|
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 };
|
return { serverId, toolName };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +50,7 @@ export function buildToolDefsFromCache(
|
|||||||
|
|
||||||
const defs: ToolDef[] = [];
|
const defs: ToolDef[] = [];
|
||||||
for (const tool of cache) {
|
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);
|
const canonical = normalizeToolName(tool.serverId, tool.toolName);
|
||||||
if (!matchesAnyPattern(canonical, mcpPatterns)) continue;
|
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 type { CachedTool } from './tool-adapter.js';
|
||||||
import { logger } from '../logger.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 {
|
interface Row {
|
||||||
server_id: string;
|
server_id: string;
|
||||||
@ -63,8 +66,8 @@ export function createToolCache(db: Database.Database, ttlSeconds: number) {
|
|||||||
VALUES (?, ?, ?, ?, datetime('now'))`,
|
VALUES (?, ?, ?, ?, datetime('now'))`,
|
||||||
);
|
);
|
||||||
for (const t of tools) {
|
for (const t of tools) {
|
||||||
if (!SLUG.test(t.name)) {
|
if (!TOOL_NAME.test(t.name)) {
|
||||||
logger.warn(`[mcp:cache] skip tool with invalid slug: ${t.name}`);
|
logger.warn(`[mcp:cache] skip tool with invalid name: ${t.name}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
insert.run(
|
insert.run(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user