ai
Version:
AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.
77 lines (70 loc) • 2.62 kB
text/typescript
import { asSchema, type ToolSet } from '@ai-sdk/provider-utils';
import { hashCanonical } from '../util/canonical-hash';
/**
* Tag a tool description for hashing. A function description is developer-owned
* (evaluated per call from local context), not a server-controlled "rug pull"
* vector, so only its presence is pinned, not its identity. The tagged shape
* keeps a literal string equal to some placeholder from ever hashing like a
* function.
*/
function tagDescription(description: unknown) {
if (typeof description === 'string') {
return { type: 'string', value: description } as const;
}
if (description == null) {
return { type: 'none' } as const;
}
return { type: 'function' } as const;
}
/**
* Fingerprint the server-controlled, security-relevant fields of each tool in a
* `ToolSet`: `description` (string form only), the resolved input JSON schema,
* and `title`. Returns a map of tool name to a stable digest.
*
* Capture a baseline at trust time (first connect, human-reviewed) and compare
* later fetches with {@link detectToolDrift} to catch MCP tool-definition drift
* ("rug pull"). Baseline storage and the drift response are the app's concern.
*/
export async function fingerprintTools(
tools: ToolSet,
): Promise<Record<string, string>> {
const entries = await Promise.all(
Object.keys(tools).map(async name => {
const tool = tools[name];
const digest = await hashCanonical({
description: tagDescription(tool.description),
inputSchema: await asSchema(tool.inputSchema).jsonSchema,
title: tool.title,
});
return [name, digest] as const;
}),
);
return Object.fromEntries(entries);
}
/**
* Pure diff of two fingerprint maps produced by {@link fingerprintTools}.
* `added`/`removed` are tools present in only one map; `changed` are tools whose
* pinned definition differs. Uses own-property lookups so a tool literally named
* `constructor` or `toString` diffs correctly.
*/
export function detectToolDrift(
current: Record<string, string>,
baseline: Record<string, string>,
): { added: string[]; removed: string[]; changed: string[] } {
const added: string[] = [];
const removed: string[] = [];
const changed: string[] = [];
for (const name of Object.keys(current)) {
if (!Object.hasOwn(baseline, name)) {
added.push(name);
} else if (current[name] !== baseline[name]) {
changed.push(name);
}
}
for (const name of Object.keys(baseline)) {
if (!Object.hasOwn(current, name)) {
removed.push(name);
}
}
return { added, removed, changed };
}