agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
247 lines • 11.6 kB
JavaScript
;
/**
* callChecks — a listing never makes a callable tool lose its checks.
*
* Pattern: the SDK's three per-tool call checks, carried by every tool the
* library hands out, plus one typed refusal.
* Role: The MCP SDK's `Client` runs three checks inside `callTool`, and it
* keeps them only for the tools of its most recent `tools/list`
* answer (1.32: `cacheToolMetadata` replaces them on every answer):
*
* 1. a tool with `execution.taskSupport: 'required'` is refused before a
* plain `tools/call` is sent;
* 2. a tool with an `outputSchema` must answer with `structuredContent`,
* unless the answer is an error;
* 3. that `structuredContent` must match the schema.
*
* So a tool that declares any of them carries them itself, as the listing it
* came from declared them. The library leaves them to the SDK only for a tool
* of a ONE-page list whose answer is still the latest, with no list request
* out (`listingGenerations.ts`), both when the call starts and when its answer
* comes in: the one-page normal case, which runs exactly as it always did.
* Every other call runs the tool's own checks, at the SDK's moments — the first
* before sending, the other two once the answer is in, before anything reads it
* — and raises the SDK's error: its own `McpError` where this arm loaded the
* SDK, otherwise an error with the same name, code and message. Those calls
* are to a tool of a paged list, a tool whose list a later answer replaced (a
* refresh that failed or succeeded; a reference held across it), and any call
* made while a list is being read.
*
* All of this covers the lists the library reads (`tools()`, `refresh()`); a
* listing it does not send — the SDK client's own automatic refresh, a
* `listTools` call of the caller's — is not seen (`listingGenerations.ts`).
*
* The output checks need a JSON Schema validator equal to the client's: the
* SDK's default (`AjvJsonSchemaValidator`, loaded with the SDK on the Node
* loader arm), or the caller's `jsonSchemaValidator`. Where neither exists — an
* `sdk` or `connection` arm given none — an answer under these rules is never
* returned unchecked (`McpOutputUncheckedError`):
*
* - a call under the tool's own checks from the start, to a tool that
* declares an `outputSchema`, is refused before it is sent (`sent: false`);
* - a call that started under the SDK's checks is refused once its answer is
* in, unread, when the library sent a `tools/list` request while the call
* was out (`sent: true`): the SDK may have checked it against another list.
*
* A schema is compiled when a tool's own checks first need it, never before:
* the one-page normal case compiles nothing twice.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.McpOutputUncheckedError = exports.checkOutput = exports.refuseRequiredTask = exports.outputUnchecked = exports.checksFor = exports.validatorOption = exports.callCheckKit = void 0;
/** JSON-RPC's codes for the two errors the SDK raises here. */
const INVALID_REQUEST = -32600;
const INVALID_PARAMS = -32602;
/**
* The kit for one connection, named `server`, whose list requests `listings`
* counts (`listingsOf(connection)`, shared by every client over it). `given` is
* the caller's `jsonSchemaValidator`; `loaded` holds what the library loaded
* with the SDK on the Node loader arm — the SDK's default validator class and
* its `McpError` — each `undefined` where it is not a constructor (another arm,
* or an SDK without it).
*/
function callCheckKit(server, listings, given, loaded = {}) {
const Default = typeof loaded.Validator === 'function' ? loaded.Validator : undefined;
let created;
return {
server,
validator: () => {
if (given !== undefined)
return given;
if (Default === undefined)
return undefined;
created ??= new Default();
return created;
},
...(typeof loaded.McpError === 'function' && { McpError: loaded.McpError }),
listings,
};
}
exports.callCheckKit = callCheckKit;
/**
* `value` as a validator, or `undefined` when it was not given. Refused by name
* when it is something else, before anything connects.
*/
function validatorOption(value, at) {
if (value === undefined)
return undefined;
const getValidator = value !== null && (typeof value === 'object' || typeof value === 'function')
? value.getValidator
: undefined;
if (typeof getValidator === 'function')
return value;
throw new TypeError(`${at}: \`jsonSchemaValidator\` has no \`getValidator\` method. Pass a JSON Schema ` +
"validator as the MCP SDK's `Client` takes one: `new AjvJsonSchemaValidator()` from " +
"'@modelcontextprotocol/sdk/validation/ajv', or the one your client uses.");
}
exports.validatorOption = validatorOption;
/**
* The checks `listed` carries — `undefined` when it declares none. `generation`
* is the one answer its list was read in, when it was read in one
* (`toolList.ts` · `ToolListing`); a paged list's tools have none, so the SDK
* never holds all of them.
*/
function checksFor(listed, kit, generation) {
const schema = ownObject(listed, 'outputSchema');
const taskRequired = ownValue(ownObject(listed, 'execution'), 'taskSupport') === 'required';
if (schema === undefined && !taskRequired)
return undefined;
let compiled;
return {
server: kit.server,
tool: listed.name,
taskRequired,
outputDeclared: schema !== undefined,
heldBySdk: () => generation !== undefined && kit.listings.isLatest(generation),
validate: () => {
if (schema !== undefined)
compiled ??= kit.validator()?.getValidator(schema);
return compiled;
},
fail: (code, message) => kit.McpError !== undefined
? new kit.McpError(code, message)
: new McpErrorShape(code, message),
};
}
exports.checksFor = checksFor;
/**
* True when this call's output cannot be checked here: it runs under the
* tool's own checks, the tool declares an `outputSchema`, no validator is
* reachable, and it is not refused as a required task anyway (that refusal is
* the SDK's, and comes first). The call is then refused before it is sent.
*/
function outputUnchecked(checks) {
return checks.outputDeclared && !checks.taskRequired && checks.validate() === undefined;
}
exports.outputUnchecked = outputUnchecked;
/** The SDK's task refusal, word for word, before anything is sent. */
function refuseRequiredTask(checks) {
if (!checks.taskRequired)
return;
throw checks.fail(INVALID_REQUEST, `Tool "${checks.tool}" requires task-based execution. ` +
'Use client.experimental.tasks.callToolStream() instead.');
}
exports.refuseRequiredTask = refuseRequiredTask;
/**
* The SDK's two output checks, in its order and with its sentences, once the
* answer is in and before anything reads it. An answer no validator here can
* check is never returned: it is refused, unread, as sent.
*/
function checkOutput(checks, result) {
if (!checks.outputDeclared)
return;
const validate = checks.validate();
if (validate === undefined)
throw new McpOutputUncheckedError(checks.server, checks.tool, true);
const structured = ownValue(result, 'structuredContent');
if (!structured && !ownValue(result, 'isError')) {
throw checks.fail(INVALID_REQUEST, `Tool ${checks.tool} has an output schema but did not return structured content`);
}
if (!structured)
return;
let mismatch;
try {
const verdict = validate(structured);
if (!verdict.valid) {
mismatch = `Structured content does not match the tool's output schema: ${verdict.errorMessage}`;
}
}
catch (error) {
throw checks.fail(INVALID_PARAMS, `Failed to validate structured content: ${error instanceof Error ? error.message : String(error)}`);
}
if (mismatch !== undefined)
throw checks.fail(INVALID_PARAMS, mismatch);
}
exports.checkOutput = checkOutput;
/**
* The SDK's `McpError`, for an arm that cannot reach the class itself (`sdk`,
* `connection`): the same name, `code`, `data` and message, so a caller that
* branches on those handles it as it handles the SDK's.
*/
class McpErrorShape extends Error {
code;
data;
constructor(code, message) {
super(`MCP error ${code}: ${message}`);
this.code = code;
this.data = undefined;
this.name = 'McpError';
}
}
/** What the refusal says happened, refused before sending. */
const NOT_SENT = 'was not sent. The tool declares an outputSchema, the MCP SDK no longer holds the checks of ' +
'the tool list it came from (that list was paged, a later list replaced it, or a list is ' +
'being read)';
/** What the refusal says happened, refused once the answer was in. */
const SENT_UNREAD = 'was sent, and its answer is not returned. The tool declares an outputSchema, a tool list ' +
'was read while the call was out (so the MCP SDK may not have checked the answer against ' +
"this tool's schema)";
/**
* A tool call whose output this connection cannot check, refused. The tool
* declares an `outputSchema`, the MCP SDK no longer holds the checks of the
* tool list it came from, and this connection has no JSON Schema validator to
* check it with — so its answer is never returned:
*
* - `sent: false` — refused before it was sent: nothing reached the server.
* The tool came from a paged list, from a list a later answer replaced, or
* the call was made while a list was being read.
* - `sent: true` — the call was sent, and a `tools/list` request went out
* before its answer came in: the SDK may have checked that answer against
* the other list, so it is refused unread. The tool may have run.
*
* Pass `jsonSchemaValidator` — the validator your client uses — to `mcpClient`.
* Names the server and the tool, never an argument or an answer.
*/
class McpOutputUncheckedError extends Error {
server;
tool;
sent;
code = 'ERR_MCP_OUTPUT_UNCHECKED';
reason = 'output-unchecked';
constructor(server, tool,
/** Whether the request left the client: `false` when the refusal came before it. */
sent) {
super(`mcpClient[${server}]: the call to '${tool}' ${sent ? SENT_UNREAD : NOT_SENT}, and this ` +
'connection has no JSON Schema validator to check its output with. Pass ' +
'`jsonSchemaValidator` to mcpClient: the validator your client uses.');
this.server = server;
this.tool = tool;
this.sent = sent;
this.name = 'McpOutputUncheckedError';
}
}
exports.McpOutputUncheckedError = McpOutputUncheckedError;
/** An own data property, never an accessor's result; `undefined` otherwise. */
function ownValue(value, key) {
if (value === null || typeof value !== 'object')
return undefined;
const descriptor = Object.getOwnPropertyDescriptor(value, key);
return descriptor !== undefined && 'value' in descriptor ? descriptor.value : undefined;
}
/** An own data property that is a plain object (not an array); `undefined` otherwise. */
function ownObject(value, key) {
const found = ownValue(value, key);
return found !== null && typeof found === 'object' && !Array.isArray(found)
? found
: undefined;
}
//# sourceMappingURL=callChecks.js.map