agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
161 lines (160 loc) • 8.52 kB
TypeScript
/**
* 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.
*/
import type { McpCallToolResult, McpJsonSchemaValidator, McpListedTool } from './types.js';
import type { ListingGenerations } from './listingGenerations.js';
/** The SDK's `McpError` constructor, as `@modelcontextprotocol/sdk/types.js` exports it. */
export type McpErrorClass = new (code: number, message: string, data?: unknown) => Error;
/** A compiled output check, as the SDK's validator returns one. */
type Validate = (input: unknown) => {
readonly valid: boolean;
readonly errorMessage?: string;
};
/**
* What one connection checks its tools' calls with: a validator equal to its
* client's, the SDK's own error class where the library loaded the SDK, and
* which `tools/list` answer the SDK holds its checks for.
*/
export interface CallCheckKit {
/** The client's name, as its refusals name it. */
readonly server: string;
/** The validator; created on first use. `undefined` when none is reachable. */
validator(): McpJsonSchemaValidator | undefined;
/** The SDK's own `McpError`, when this arm loaded the SDK. */
readonly McpError?: McpErrorClass;
/** This connection's list requests, counted (`listingGenerations.ts` · `listingsOf`). */
readonly listings: ListingGenerations;
}
/**
* 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).
*/
export declare function callCheckKit(server: string, listings: ListingGenerations, given: McpJsonSchemaValidator | undefined, loaded?: {
readonly Validator?: unknown;
readonly McpError?: unknown;
}): CallCheckKit;
/**
* `value` as a validator, or `undefined` when it was not given. Refused by name
* when it is something else, before anything connects.
*/
export declare function validatorOption(value: unknown, at: string): McpJsonSchemaValidator | undefined;
/** The checks one listed tool carries, as the listing it came from declared them. */
export interface CallChecks {
readonly server: string;
readonly tool: string;
/** `execution.taskSupport: 'required'`: only ever run as a task. */
readonly taskRequired: boolean;
/** The tool declares an `outputSchema`. */
readonly outputDeclared: boolean;
/**
* True while the SDK's `Client` holds this tool's checks: the tool came from
* a one-page list whose answer is still the latest, and no list request is
* out. Then the SDK checks the call, and nothing here runs.
*/
heldBySdk(): boolean;
/**
* The compiled output check, compiled from the declared schema on first use.
* `undefined` when no output is declared or no validator is reachable.
*/
validate(): Validate | undefined;
/** Raise an SDK error, as this arm can: the SDK's class, or its exact shape. */
fail(code: number, message: string): Error;
}
/**
* 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.
*/
export declare function checksFor(listed: McpListedTool, kit: CallCheckKit, generation: number | undefined): CallChecks | undefined;
/**
* 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.
*/
export declare function outputUnchecked(checks: CallChecks): boolean;
/** The SDK's task refusal, word for word, before anything is sent. */
export declare function refuseRequiredTask(checks: CallChecks): void;
/**
* 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.
*/
export declare function checkOutput(checks: CallChecks, result: McpCallToolResult): void;
/**
* 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.
*/
export declare class McpOutputUncheckedError extends Error {
readonly server: string;
readonly tool: string;
/** Whether the request left the client: `false` when the refusal came before it. */
readonly sent: boolean;
readonly code: "ERR_MCP_OUTPUT_UNCHECKED";
readonly reason: "output-unchecked";
constructor(server: string, tool: string,
/** Whether the request left the client: `false` when the refusal came before it. */
sent: boolean);
}
export {};