UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

214 lines (213 loc) • 12.1 kB
/** * toolExtras — agentfootprint's own tool DECLARATIONS, carried over MCP. * * A `Tool` is two different things at once. Half of it is EXECUTION — the * handler, the credential it needs, the human it wants to check in with. The * other half is DECLARATION — flat, inert facts about the tool that no code * runs and every consumer-side rail READS: where its arguments come from, what * kind of artifact its result is, who owns it, what class of answer it gives, * how big a result is too big. * * Over MCP only the execution half used to cross. A tool served by `mcpServe` * arrived at a client as a name, a description and an input schema, so every * rail that reads a declaration went quiet for it: the dangling-reference and * unsupported-argument checks never armed, placement minted * `tool-result/<name>` that no `wants` could spend, and the identity joins had * no subject. An MCP server could be somebody's whole tool catalogue and still * be a second-class citizen of every check this library ships. * * ── The carrier ───────────────────────────────────────────────────────── * MCP's own extensibility bag: `Tool._meta`, under ONE namespaced key * ({@link MCP_TOOL_EXTRAS_KEY}). `_meta` is declared on the spec's `Tool` * object (`@modelcontextprotocol/sdk` types it as * `z.record(z.string(), z.unknown()).optional()` on `ToolSchema`), so it * survives the SDK's own `tools/list` validation in both directions and needs * no new protocol, no side channel and no version negotiation. A server that * has never heard of agentfootprint sends no bag; a client that has never * heard of it ignores the key. * * ── What may travel, and the bar ──────────────────────────────────────── * **A declaration a consumer-side check or rail reads; nothing that governs * execution.** Every field below passes it. `needs` (credentials), `checkIn` * (human consent) and the session hooks do NOT and never will: they govern how * a tool RUNS, and the tool runs on the server. Sending them would tell a * client to hold a gate that the only executor of the tool has already held, * or — worse — that nobody is holding at all. `mcpServe` already refuses to * serve a `checkIn` tool for exactly that reason; this list is the same law, * stated on the way out. * * ── The asymmetry that matters ────────────────────────────────────────── * SERVING validates nothing new: the tool was built by `defineTool`, which * already refused every malformed declaration at definition time. * * INGEST validates everything and THROWS NOTHING. A bag comes from a server * this process does not control, and one server's typo must not take down a * bulk register of forty tools. So each field is judged by the SAME rule * `defineTool` enforces (literally the same exported assert), and a field that * fails is warned about once — naming the server, the tool, the field and the * rule — and DROPPED. The tool still registers, without that one declaration. * That is the honest outcome: the rail it would have armed stays unarmed, and * the developer is told which rail and why. */ import { type Tool, type ToolOwner, type ToolResultCeiling, type ToolResultColumns } from '../tools/tools.js'; import type { ToolResultClass } from '../semantics/types.js'; import { type AskOrAssume, type ToolPeriod } from '../tools/declare.js'; import { type ToolScript } from '../tools/script.js'; import type { ToolSearch } from '../semantics/coverage/search/types.js'; import { type TimeContext } from '../time/wire.js'; /** * The single `_meta` key every agentfootprint declaration travels under. * * Namespaced so it can never collide with another vendor's `_meta` entry, and * a CONSTANT so there is exactly one spelling in the codebase — the * `branchSegment.ts` precedent upstream: one owner of the grammar, and nobody * else may write the string. */ export declare const MCP_TOOL_EXTRAS_KEY = "agentfootprint"; /** * The declarations that cross an MCP boundary — the plain-data half of a * `Tool`, in the shape the receiving side puts straight back onto a `Tool`. * * Every field is optional and ABSENT MEANS ABSENT: a tool that declares * nothing sends no bag at all, and a tool that declares two sends two keys, * never five with three empty placeholders. "Said nothing" and "said empty" * are different statements everywhere else in this library, and they are * different here. * * @see MCP_TOOL_EXTRAS_KEY for the `_meta` key it rides under. */ export interface McpToolExtras { /** Where this tool's arguments come from — see {@link Tool.argumentsFrom}. * Arms the dangling-reference and unsupported-argument checks. */ readonly argumentsFrom?: readonly string[]; /** The artifact kind a PLACED result is minted under — see * {@link Tool.resultKind}. What makes a placed result spendable by `wants`. */ readonly resultKind?: string; /** * What this tool's ROWS contain (9.78.0) — see {@link Tool.resultColumns}. * Arms the write seam's `column-type-mismatch` and `missing-column` checks. * * It clears the bar this file states, and by the widest margin of the eight. * It is a flat, inert fact about the RESULT that a consumer-side check * reads; it governs nothing about how the tool runs, and the tool runs on * the server. It is also the field case for carrying declarations at all: a * remote catalogue of rowset tools is exactly where a numeric column * arriving as text goes unnoticed, because the consumer holding the chart * has no way to know what the producer meant. Leaving it behind would arm * the check for local tools and leave every MCP tool a second-class citizen * of it — the whole defect `toolExtras` was built to end. */ readonly resultColumns?: ToolResultColumns; /** The identity edge integrity checks join on — see {@link Tool.owner}. */ readonly owner?: ToolOwner; /** The declared class of this tool's results — see {@link Tool.resultClass}. */ readonly resultClass?: ToolResultClass; /** The author's refusing ceiling on this tool's result — see * {@link Tool.resultCeiling}. */ readonly resultCeiling?: ToolResultCeiling; /** The named ingredient tools this tool is composed of (9.76.0) — see * {@link Tool.composedOf}. Feeds the receiving agent's build-time drift * gate. */ readonly composedOf?: readonly string[]; /** Whether this tool's procedure can raise an approval gate (9.76.0) — see * {@link Tool.gates}. Read by composition-time checks that must keep a * gating tool out of a fan-out branch. */ readonly gates?: boolean; /** * The per-argument rules (the inputs layer, honesty layer 2) — see * {@link Tool.askOrAssume}. It passes this file's bar: the fill and the * batch ask happen in the CLIENT's own loop before the call * is sent — exactly as the client applies a served `resultCeiling` — and * nothing about how the server runs the tool changes. The FIRST extra * judged against the tool's schema, which is why the origin carries the * listed tool's `inputSchema`. A client that ignores `_meta` sees the * author's `required` and sends the value, or the server applies its own * default. */ readonly askOrAssume?: AskOrAssume; /** Which arguments set the period the answer covers, the forms they take * and the facts about the source — see {@link Tool.period}. Judged * against the listed tool's own `inputSchema` and the KEPT `askOrAssume` * (every argument a form names must carry a kept rule). A host tool * written in another language declares the same object, field for field. */ readonly period?: ToolPeriod; /** * What one call of this tool searches — see {@link Tool.search}. It passes * this file's bar: composition is a consumer-side reading of the call's * arguments and governs nothing about how the server runs the tool. Judged * against the listed tool's own `inputSchema` and the KEPT `period`. A kept * remote declaration composes rows marked `composed: 'remote'`, which * establish nothing: the server's words are untrusted. */ readonly search?: ToolSearch; /** * This tool RUNS a program, carried by its one string argument `argument` * — see {@link Tool.script}. It passes this file's bar: a consumer-side * record reads it (which program a call submitted, matched against what the * answer shows) and it governs nothing about how the server runs the tool. * Judged against the listed tool's own `inputSchema`. */ readonly script?: ToolScript; } /** * The `_meta` bag a `tools/call` request carries for a call handed * `ctx.time` (the time layer, `time/wire.ts` · `TimeContext`): the * same object, under the one namespaced key — so a host tool in another * process can declare the `period.queried` its read covered without parsing * its own argument. */ export declare function callMetaOf(time: TimeContext): Readonly<Record<string, unknown>>; /** * The call's time a `tools/call` request carried in its `_meta` bag, read — * or `undefined` when it carried none, or one this runtime cannot read (a * different version, a malformed field): never repaired, never guessed. What * `mcpServe` hands a served tool as `ctx.time`. */ export declare function callTimeOf(meta: unknown): TimeContext | undefined; /** * The declarations on `tool`, ready to be written into an MCP `_meta` bag — * or `undefined` when the tool declares none, so `mcpServe` can leave `_meta` * off the listing entirely rather than sending an empty object. * * Copied verbatim, never inferred or defaulted. This is the SERVE side, and * the values were already vetted by `defineTool`. */ export declare function toolExtrasOf(tool: Tool): McpToolExtras | undefined; /** Where a rejected declaration came from, for the warning that names it. */ export interface McpToolExtrasOrigin { /** The client's logical name — `mcpClient({ name })`. */ readonly server: string; /** The tool the bag was attached to. */ readonly tool: string; /** * The listed tool's own `inputSchema` — what `askOrAssume` and `period` are * judged against (each ruled property by its OWN schema). Absent → those * two fields are judged against an empty schema, so every rule is dropped * with a warning rather than trusted unjudged. */ readonly inputSchema?: Readonly<Record<string, unknown>>; } /** * Read an MCP `Tool._meta` bag into declarations a `Tool` can carry. * * **Never throws.** Everything here is somebody else's data: * * - no `_meta`, no `agentfootprint` key, or a non-object under it → `{}`, * and the ingest is byte-identical to every release before this existed; * - a field the receiving side does not know → ignored in silence (a NEWER * server talking to an OLDER client is not an error, and warning about it * would make every upgrade noisy at the wrong end); * - a field this library DOES know but cannot honour → warned once and * dropped, and the rest of the bag still lands. * * The rule each field is judged by is the exported assert `defineTool` itself * calls, so a declaration that would have been refused at definition time is * refused here too, with the same sentence. */ export declare function readToolExtras(meta: unknown, origin: McpToolExtrasOrigin): McpToolExtras; /** * Forget every warning issued so far. * @internal test seam — the ledger is process-wide and warn-ONCE, so a test * proving the warning fires must be able to start from silence. */ export declare function _resetToolExtrasWarnings(): void;