UNPKG

kestrel.markets

Version:

A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.

168 lines (147 loc) 9.14 kB
/** * # mcp/protocol — the JSON-RPC 2.0 + MCP wire vocabulary the Kestrel MCP face speaks (kestrel-djm.7) * * The small, dependency-free wire surface the {@link ./server.ts KestrelMcpServer} dispatcher answers over: * the JSON-RPC 2.0 envelope (`resources/list` · `resources/read` · `tools/list` · `tools/call` · the * `initialize` handshake) plus the MCP resource / tool result shapes. This module holds NO Kestrel runtime * semantics and imports NOTHING — it is pure types + own constants (the same zero-dependency discipline the * djm.2 protocol modules keep). Every djm.2/djm.4 protocol OBJECT the face returns rides VERBATIM inside a * {@link ToolResult.structuredContent} or a {@link ResourceContents.text}; this module only frames it. * * ── DESIGN FORK LOGGED (hard rules forbid .beads writes — recorded here like djm.5's sdk/types.ts note) ── * (1) TRANSPORT: hand-rolled JSON-RPC 2.0, NOT `@modelcontextprotocol/sdk` (absent from node_modules; a heavy * dep on the package graph the 'mcp' keyword advertises but never installed). The MCP wire is a small * JSON-RPC 2.0 surface a zero-dependency dispatcher serves — no new dep, no drag on the light/heavy seam. * (2) RESOURCE URIs: `kestrel://catalog/<index>` (the static djm.8 CatalogEntry mirror the SDK's `catalog()` * returns — the tail is the enumeration index; the catalog is compared order-independently) and * `kestrel://artifact/<encodeURIComponent(ref)>` (the immutable, content-addressed finalized artifacts, * addressed by the SDK's own artifact `ref`). A resource READ returns the djm.2 object VERBATIM as JSON. * (3) TOOL NAMING: dot-namespaced under `kestrel.` — one tool per SDK verb ({@link TOOL}). * (4) FAILURE TAXONOMY: a TRANSPORT failure (malformed frame / bad request) → a JSON-RPC ERROR * ({@link PARSE_ERROR} / {@link INVALID_REQUEST}); a KESTREL REFUSAL (a typed SDK error) → a tool RESULT * with `isError:true` + `structuredContent.refusal.code`; a STAND_DOWN → a normal tool RESULT. The three * are distinct, caller-distinguishable shapes (see the server's dispatch). */ /* ─────────────────────────────── JSON-RPC 2.0 envelope ─────────────────────── */ /** A JSON-RPC 2.0 request/response id — a number, a string, or `null` (the reserved id for a request that * could not be correlated, e.g. a frame that failed to parse). */ export type JsonRpcId = number | string | null; /** A parsed JSON-RPC 2.0 request the dispatcher answers. `params` is deliberately `unknown` — every method * narrows it fail-closed (a missing/ill-typed field is a typed refusal or an invalid-params error). */ export interface JsonRpcRequest { readonly jsonrpc: "2.0"; readonly id?: JsonRpcId; readonly method: string; readonly params?: unknown; } /** The JSON-RPC 2.0 error object (the TRANSPORT-layer failure signal). `code` is a JSON-RPC reserved code. */ export interface JsonRpcError { readonly code: number; readonly message: string; readonly data?: unknown; } /** The JSON-RPC 2.0 response envelope: success carries `result`, a transport failure carries `error` * (never both). A domain (Kestrel) refusal is NOT an `error` — it rides as an `isError` {@link ToolResult}. */ export interface JsonRpcResponse { readonly jsonrpc: "2.0"; readonly id: JsonRpcId; readonly result?: unknown; readonly error?: JsonRpcError; } /* ─────────────────────── JSON-RPC 2.0 reserved error codes ─────────────────── */ /** Invalid JSON was received — the TRANSPORT signal for a malformed MCP frame (the broken-pipe class). */ export const PARSE_ERROR = -32700; /** The JSON was not a valid JSON-RPC Request object. */ export const INVALID_REQUEST = -32600; /** The method does not exist / is not available. */ export const METHOD_NOT_FOUND = -32601; /** Invalid method parameter(s). */ export const INVALID_PARAMS = -32602; /** Internal JSON-RPC error (an unexpected server fault — kept DISTINCT from a typed Kestrel refusal). */ export const INTERNAL_ERROR = -32603; /* ───────────────────────────── MCP resource shapes ─────────────────────────── */ /** The resource URI schemes (see the fork note): the static djm.8 catalog listings, the immutable artifacts, * and the SINGLE ex-ante catalog pricing block (kestrel-adge) when the catalog serves one. */ export const CATALOG_URI_PREFIX = "kestrel://catalog/"; export const ARTIFACT_URI_PREFIX = "kestrel://artifact/"; /** The one ex-ante pricing resource URI (kestrel-adge) — advertised only when `sdk.catalog()` carries a * `pricing` block; reading it returns the {@link import("../protocol/catalog.ts").CatalogPricing} verbatim. */ export const PRICING_URI = "kestrel://catalog-pricing"; /** Build the immutable-artifact URI for a content-addressed artifact `ref` (the SDK's own artifact handle). */ export const artifactUri = (ref: string): string => `${ARTIFACT_URI_PREFIX}${encodeURIComponent(ref)}`; /** A resource advertised by `resources/list`. */ export interface Resource { readonly uri: string; readonly name: string; readonly description?: string; readonly mimeType?: string; } /** The lossless payload a `resources/read` returns — the djm.2 protocol object serialized VERBATIM as JSON * text (`application/json`). The caller `JSON.parse`s `text` back to the exact protocol object. */ export interface ResourceContents { readonly uri: string; readonly mimeType?: string; readonly text: string; } /* ──────────────────────────────── MCP tool shapes ──────────────────────────── */ /** The tool names — one tool per SDK verb, dot-namespaced under `kestrel.`. Nothing the SDK can do is * missing from the MCP face (the complete trial + incremental Session lifecycle + Grade + continuation). */ export const TOOL = { validate: "kestrel.validate", open: "kestrel.session.open", start: "kestrel.session.start", advance: "kestrel.session.advance", revise: "kestrel.session.revise", submit: "kestrel.session.submit", resume: "kestrel.session.resume", finalize: "kestrel.session.finalize", grade: "kestrel.grade", operationResume: "kestrel.operation.resume", // ── The LOCAL operator-secret store (kestrel-jh9w.3). Machine-first parity with the `kestrel secrets` // CLI verb: an agent self-installs its BYOK keys ONCE into `~/.kestrel/.env` and they persist across // context windows / worktrees (credential residency). set/unset/list are the ONLY three verbs — there is // deliberately no `get`: NO face ever returns a secret VALUE, only internal resolution reads them. secretsSet: "kestrel.secrets.set", secretsUnset: "kestrel.secrets.unset", secretsList: "kestrel.secrets.list", } as const; /** The frozen tool-name space. */ export type ToolName = (typeof TOOL)[keyof typeof TOOL]; /** A single content block on a tool result (the human-readable rendering; the machine-readable truth is * {@link ToolResult.structuredContent}). */ export interface ContentBlock { readonly type: "text"; readonly text: string; } /** * A `tools/call` RESULT. `structuredContent` carries the lossless djm.2 protocol object VERBATIM (the * canonical identities / diagnostics / receipts / roots — never a reshaped summary). `isError:true` marks a * TYPED KESTREL REFUSAL (its `structuredContent.refusal.code` is the typed code the caller matches on) — a * domain outcome delivered IN-BAND, never conflated with a JSON-RPC transport `error`. */ export interface ToolResult { readonly content: readonly ContentBlock[]; readonly structuredContent?: unknown; readonly isError?: boolean; } /** A tool advertised by `tools/list`: its name, a description, and its JSON-Schema input shape. */ export interface ToolDescriptor { readonly name: string; readonly description: string; readonly inputSchema: Record<string, unknown>; } /* ───────────────────────── the MCP protocol-error carrier ──────────────────── */ /** * A TRANSPORT-layer protocol error the dispatcher raises for a malformed frame / bad request / invalid * params — carried out of a handler and mapped to a JSON-RPC `error` with {@link jsonRpcCode}. Deliberately * carries NO `code: string` field, so it is NEVER mistaken for a typed Kestrel refusal (which the tool path * detects by a string `code`): the two failure classes stay distinct and caller-distinguishable (fail-closed). */ export class McpProtocolError extends Error { override readonly name = "McpProtocolError"; readonly jsonRpcCode: number; constructor(jsonRpcCode: number, message: string) { super(message); this.jsonRpcCode = jsonRpcCode; } }