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.
476 lines (471 loc) • 19.2 kB
JavaScript
import {
AUTHORED_RESPONSE_SHAPE,
BOUND_RESPONSE_SHAPE,
EVENT_CURSOR_SHAPE,
SDK_VERSION,
SUBJECT_SHAPE,
SdkLocalError,
createSdk,
localTransport,
remoteTransport
} from "./bin-a4jbxqxk.js";
import"./bin-cv13fq8c.js";
import"./bin-kxzrsgjw.js";
import {
DEFAULT_API
} from "./bin-qewrvqtv.js";
import {
parseArgs
} from "./bin-1gc4zavq.js";
import {
CliError,
EXIT
} from "./bin-t9ggwnv5.js";
import {
checkOperatorSecretKey,
defaultSecretsPath,
deleteSecret,
saveSecret,
secretNames
} from "./bin-q0g6xvsw.js";
import"./bin-xx1qrxe3.js";
import"./bin-t58k60v9.js";
import"./bin-p5jfg5b0.js";
import"./bin-29be75ss.js";
import"./bin-n77kea5n.js";
import"./bin-mn7wcxhv.js";
import"./bin-8pchjxn2.js";
import"./bin-am3351y2.js";
import"./bin-73jr945c.js";
import"./bin-df1tn094.js";
import"./bin-3ft0k1jq.js";
import"./bin-2ywrx58g.js";
import"./bin-wckvcay0.js";
// src/mcp/protocol.ts
var PARSE_ERROR = -32700;
var INVALID_REQUEST = -32600;
var METHOD_NOT_FOUND = -32601;
var INVALID_PARAMS = -32602;
var INTERNAL_ERROR = -32603;
var CATALOG_URI_PREFIX = "kestrel://catalog/";
var ARTIFACT_URI_PREFIX = "kestrel://artifact/";
var PRICING_URI = "kestrel://catalog-pricing";
var artifactUri = (ref) => `${ARTIFACT_URI_PREFIX}${encodeURIComponent(ref)}`;
var 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",
secretsSet: "kestrel.secrets.set",
secretsUnset: "kestrel.secrets.unset",
secretsList: "kestrel.secrets.list"
};
class McpProtocolError extends Error {
name = "McpProtocolError";
jsonRpcCode;
constructor(jsonRpcCode, message) {
super(message);
this.jsonRpcCode = jsonRpcCode;
}
}
// src/mcp/server.ts
function createKestrelMcpServer(options) {
return new KestrelMcpServerImpl(options.transport);
}
var sessionIdSchema = { type: "string", description: "the durable SessionId returned by kestrel.session.open" };
var authoredSchema = {
type: "object",
description: `an AuthoredResponse — ${AUTHORED_RESPONSE_SHAPE}`
};
function tool(name, description, properties, required) {
return { name, description, inputSchema: { type: "object", properties, required, additionalProperties: false } };
}
var TOOL_DESCRIPTORS = [
tool(TOOL.validate, "Parse + validate a Kestrel document; returns the { ok, diagnostics } ValidateOutcome verbatim.", {
document: { type: "string", description: "the Kestrel document source" }
}, ["document"]),
tool(TOOL.open, "Open an incremental Session over a subject; returns { gated:false, sessionId } or the 402/Offer as { gated:true, payment }.", {
subject: { type: "object", description: `a SessionRef — ${SUBJECT_SHAPE}` }
}, ["subject"]),
tool(TOOL.start, "Deliver the date-blind OPEN Frame for a live Session; returns the djm.4 Delivery verbatim — its `frame` is the TYPED SessionFrame struct (not rendered text; `frameRoot` = sha256 of its canonical JSON).", {
sessionId: sessionIdSchema
}, ["sessionId"]),
tool(TOOL.advance, "Author-or-stand-down at the pending slot; returns the djm.4 SessionResponse verbatim (receipt + next Delivery carrying the typed SessionFrame struct, or a typed refusal).", {
sessionId: sessionIdSchema,
response: authoredSchema
}, ["sessionId", "response"]),
tool(TOOL.revise, "Author a superseding document at the current slot; returns the SessionResponse verbatim.", {
sessionId: sessionIdSchema,
response: authoredSchema
}, ["sessionId", "response"]),
tool(TOOL.submit, "Explicit-binding turn: reconcile a fully-bound BoundResponse against the recorded slot; returns the SessionResponse verbatim.", {
sessionId: sessionIdSchema,
response: { type: "object", description: `a BoundResponse — ${BOUND_RESPONSE_SHAPE}` }
}, ["sessionId", "response"]),
tool(TOOL.resume, "Re-derive the committed pentad-chained transcript; returns the SessionTranscript verbatim.", {
sessionId: sessionIdSchema,
after: { type: "object", description: `optional resume cursor — an EventCursor ${EVENT_CURSOR_SHAPE}` }
}, ["sessionId"]),
tool(TOOL.finalize, "Seal the chain; returns { blotter, sessionId, tipHash, conformanceRoot, artifacts } verbatim.", {
sessionId: sessionIdSchema
}, ["sessionId"]),
tool(TOOL.grade, "Grade one or more finalized Blotter artifacts; returns the Gated<GradeOutcome> verbatim (may gate as DATA).", {
blotters: { type: "array", items: { type: "string" }, description: "Blotter artifact ids (a Blotter's sessionId is its id)" }
}, ["blotters"]),
tool(TOOL.operationResume, "Resume a durable Operation from its cursor; returns the OperationResumption { cursor, payload } verbatim.", {
operationId: { type: "string", description: "the durable Operation id" },
after: { type: "object", description: `optional resume cursor — an EventCursor ${EVENT_CURSOR_SHAPE}` }
}, ["operationId"]),
tool(TOOL.secretsSet, "Store one operator/BYOK secret in the local ~/.kestrel/.env store (owner-only 0600), keyed by a canonical uppercase env name. The VALUE is never echoed back — the result names only { key, path, stored:true }. Refuses a *LIVE* key (paper store is paper-only) and a non-[A-Z_][A-Z0-9_]* name.", {
key: { type: "string", description: "the canonical UPPERCASE env-var name, e.g. DATABENTO_API_KEY" },
value: { type: "string", description: "the secret value to store — never returned by any face; only internal resolution reads it" }
}, ["key", "value"]),
tool(TOOL.secretsUnset, "Remove one operator secret from the local ~/.kestrel/.env store; returns { key, path, removed:true }. An absent key is a typed NOT_FOUND refusal (never a silent success).", {
key: { type: "string", description: "the secret name to remove" }
}, ["key"]),
tool(TOOL.secretsList, "List the NAMES held in the local ~/.kestrel/.env store, sorted; returns { path, names }. Names only — no code path on this tool ever holds a value to render.", {}, [])
];
class KestrelMcpServerImpl {
sdk;
sessions = new Map;
producedArtifacts = new Set;
constructor(transport) {
this.sdk = createSdk(transport);
}
async handleFrame(rawLine) {
let parsed;
try {
parsed = JSON.parse(rawLine);
} catch (e) {
return errorResponse(null, PARSE_ERROR, `malformed JSON-RPC frame: ${messageOf(e)}`);
}
if (!isJsonRpcRequest(parsed)) {
const id = isRecord(parsed) && isId(parsed["id"]) ? parsed["id"] : null;
return errorResponse(id, INVALID_REQUEST, "not a JSON-RPC 2.0 request (missing string `method`)");
}
return this.handle(parsed);
}
async handle(request) {
if (!("id" in request) || request.id === undefined)
return;
const id = isId(request.id) ? request.id : null;
try {
switch (request.method) {
case "initialize":
return okResponse(id, this.initialize());
case "resources/list":
return okResponse(id, await this.listResources());
case "resources/read":
return okResponse(id, await this.readResource(fieldOf(request.params, "uri")));
case "tools/list":
return okResponse(id, { tools: TOOL_DESCRIPTORS });
case "tools/call":
return okResponse(id, await this.callTool(request.params));
default:
return errorResponse(id, METHOD_NOT_FOUND, `unknown method ${JSON.stringify(request.method)}`);
}
} catch (e) {
if (e instanceof McpProtocolError)
return errorResponse(id, e.jsonRpcCode, e.message);
return errorResponse(id, INTERNAL_ERROR, messageOf(e));
}
}
initialize() {
return {
protocolVersion: "2024-11-05",
capabilities: { resources: {}, tools: {} },
serverInfo: { name: "kestrel.markets/mcp", version: SDK_VERSION }
};
}
async listResources() {
const { entries, pricing } = await this.sdk.catalog();
const resources = entries.map((_entry, index) => ({
uri: `${CATALOG_URI_PREFIX}${index}`,
name: `kestrel-catalog-entry-${index}`,
description: "A catalog discovery listing (id + title + free-ness); openSession by its `id` for the run surface.",
mimeType: "application/json"
}));
if (pricing !== undefined) {
resources.push({
uri: PRICING_URI,
name: "kestrel-catalog-pricing",
description: `The ex-ante price sheet (every purchasable SKU's price/asset/what-you-get + settlement rails); the machine-readable answer to "what can I buy, and for how much" without provoking a 402.`,
mimeType: "application/json"
});
}
for (const ref of this.producedArtifacts) {
resources.push({
uri: artifactUri(ref),
name: `kestrel-artifact-${ref}`,
description: "An immutable, content-addressed finalized Session artifact (Blotter / conformance root); read it by this URI for the djm.2 ArtifactResult { ref, kind, value } verbatim.",
mimeType: "application/json"
});
}
return { resources };
}
async readResource(uri) {
if (typeof uri !== "string")
throw new McpProtocolError(INVALID_PARAMS, "resources/read requires a string `uri`");
if (uri === PRICING_URI) {
const { pricing } = await this.sdk.catalog();
if (pricing === undefined)
throw new McpProtocolError(INVALID_PARAMS, `no catalog pricing resource ${JSON.stringify(uri)}`);
return { contents: [jsonContents(uri, pricing)] };
}
if (uri.startsWith(CATALOG_URI_PREFIX)) {
const index = Number.parseInt(uri.slice(CATALOG_URI_PREFIX.length), 10);
const { entries } = await this.sdk.catalog();
const entry = Number.isInteger(index) && index >= 0 ? entries[index] : undefined;
if (entry === undefined)
throw new McpProtocolError(INVALID_PARAMS, `no catalog resource ${JSON.stringify(uri)}`);
return { contents: [jsonContents(uri, entry)] };
}
if (uri.startsWith(ARTIFACT_URI_PREFIX)) {
const ref = decodeURIComponent(uri.slice(ARTIFACT_URI_PREFIX.length));
const artifact = await this.sdk.artifact(ref);
return { contents: [jsonContents(uri, artifact)] };
}
throw new McpProtocolError(INVALID_PARAMS, `unknown resource uri ${JSON.stringify(uri)}`);
}
async callTool(params) {
const name = asString(fieldOf(params, "name"), "name");
const args = asRecord(fieldOf(params, "arguments"));
try {
const structuredContent = await this.invoke(name, args);
return { content: [textBlock(structuredContent)], structuredContent };
} catch (e) {
if (isKestrelRefusal(e)) {
const refusal = { code: e.code, message: e.message };
return { content: [textBlock({ refusal })], structuredContent: { refusal }, isError: true };
}
throw e;
}
}
async invoke(name, args) {
switch (name) {
case TOOL.validate:
return this.sdk.validate(asString(args["document"], "document"));
case TOOL.open: {
const opened = await this.sdk.openSession(asSessionRef(args["subject"], "subject"));
if (opened.gated)
return { gated: true, payment: opened.payment };
this.sessions.set(opened.value.sessionId, opened.value);
return { gated: false, sessionId: opened.value.sessionId };
}
case TOOL.start:
return this.session(args["sessionId"]).start();
case TOOL.advance:
return this.session(args["sessionId"]).advance(asAuthoredResponse(args["response"], "response"));
case TOOL.revise:
return this.session(args["sessionId"]).revise(asAuthoredResponse(args["response"], "response"));
case TOOL.submit:
return this.session(args["sessionId"]).submit(asBoundResponse(args["response"], "response"));
case TOOL.resume:
return this.session(args["sessionId"]).resume(asEventCursor(args["after"], "after"));
case TOOL.finalize: {
const finalized = await this.session(args["sessionId"]).finalize();
for (const ref of finalized.artifacts)
this.producedArtifacts.add(ref);
return finalized;
}
case TOOL.grade:
return this.sdk.grade({ blotters: asBlotterIds(args["blotters"], "blotters") });
case TOOL.operationResume: {
const operationId = asString(args["operationId"], "operationId");
const after = asEventCursor(args["after"], "after");
return this.sdk.resumeOperation(after === undefined ? { operationId } : { operationId, after });
}
case TOOL.secretsSet:
return this.setSecret(asString(args["key"], "key"), asString(args["value"], "value"));
case TOOL.secretsUnset:
return this.unsetSecret(asString(args["key"], "key"));
case TOOL.secretsList:
return this.listSecrets();
default:
throw new McpProtocolError(INVALID_PARAMS, `unknown tool ${JSON.stringify(name)}`);
}
}
session(sessionId) {
const id = typeof sessionId === "string" ? sessionId : undefined;
const session = id === undefined ? undefined : this.sessions.get(id);
if (session === undefined) {
throw new SdkLocalError("unknown-session", `no open MCP Session for id ${JSON.stringify(sessionId)} — call ${TOOL.open} first`);
}
return session;
}
setSecret(key, value) {
const violation = checkOperatorSecretKey(key);
if (violation !== undefined) {
throw new SdkLocalError(violation.code, `${violation.message} — ${violation.hint}`);
}
if (value.length === 0) {
throw new SdkLocalError("SECRET_EMPTY", `refusing to store an EMPTY value for ${JSON.stringify(key)} — to remove it instead call ${TOOL.secretsUnset}`);
}
const path = defaultSecretsPath();
saveSecret(key, value, path);
return { schema: "kestrel.secrets.set/v1", key, path, stored: true };
}
unsetSecret(key) {
const path = defaultSecretsPath();
if (!deleteSecret(key, path)) {
throw new SdkLocalError("NOT_FOUND", `no secret named ${JSON.stringify(key)} in ${path} — call ${TOOL.secretsList} for the stored names`);
}
return { schema: "kestrel.secrets.unset/v1", key, path, removed: true };
}
listSecrets() {
const path = defaultSecretsPath();
return { schema: "kestrel.secrets.list/v1", path, names: secretNames(path) };
}
}
function okResponse(id, result) {
return { jsonrpc: "2.0", id, result };
}
function errorResponse(id, code, message) {
return { jsonrpc: "2.0", id, error: { code, message } };
}
function textBlock(value) {
return { type: "text", text: safeJson(value) };
}
function jsonContents(uri, value) {
return { uri, mimeType: "application/json", text: safeJson(value) };
}
function safeJson(value) {
return JSON.stringify(value ?? null);
}
function messageOf(e) {
return e instanceof Error ? e.message : String(e);
}
function isRecord(v) {
return typeof v === "object" && v !== null;
}
function isId(v) {
return typeof v === "number" || typeof v === "string" || v === null;
}
function isJsonRpcRequest(v) {
return isRecord(v) && typeof v["method"] === "string";
}
function fieldOf(params, key) {
return isRecord(params) ? params[key] : undefined;
}
function asString(v, field) {
if (typeof v !== "string")
throw new McpProtocolError(INVALID_PARAMS, `expected string \`${field}\`, got ${v === null ? "null" : typeof v}`);
return v;
}
function asRecord(v) {
return isRecord(v) ? v : {};
}
function describeArg(v) {
return v === null ? "null" : Array.isArray(v) ? "array" : typeof v;
}
function asSessionRef(v, field) {
if (!isRecord(v) || typeof v["kind"] !== "string") {
throw new McpProtocolError(INVALID_PARAMS, `expected \`${field}\` to be a SessionRef object with a string \`kind\`, got ${describeArg(v)}`);
}
return v;
}
function asAuthoredResponse(v, field) {
if (!isRecord(v) || typeof v["kind"] !== "string") {
throw new McpProtocolError(INVALID_PARAMS, `expected \`${field}\` to be an AuthoredResponse object with a string \`kind\`, got ${describeArg(v)}`);
}
return v;
}
function asBoundResponse(v, field) {
if (!isRecord(v)) {
throw new McpProtocolError(INVALID_PARAMS, `expected \`${field}\` to be a BoundResponse object, got ${describeArg(v)}`);
}
return v;
}
function asBlotterIds(v, field) {
if (!Array.isArray(v) || !v.every((id) => typeof id === "string")) {
throw new McpProtocolError(INVALID_PARAMS, `expected \`${field}\` to be an array of Blotter id strings, got ${describeArg(v)}`);
}
return v;
}
function asEventCursor(v, field) {
if (v === undefined)
return;
if (!isRecord(v) || typeof v["sequence"] !== "number" || typeof v["token"] !== "string") {
throw new McpProtocolError(INVALID_PARAMS, `expected \`${field}\` to be an EventCursor { sequence:number, token:string }, got ${describeArg(v)}`);
}
return v;
}
function isKestrelRefusal(e) {
return e instanceof SdkLocalError || e instanceof Error && typeof e.code === "string";
}
// src/mcp/stdio.ts
function serveStdio(server) {
const input = process.stdin;
const output = process.stdout;
input.setEncoding("utf8");
let buffer = "";
let chain = Promise.resolve();
const enqueue = (line) => {
chain = chain.then(async () => {
const response = await server.handleFrame(line);
if (response !== undefined)
output.write(JSON.stringify(response) + `
`);
});
};
return new Promise((resolve, reject) => {
input.on("data", (chunk) => {
buffer += String(chunk);
let nl;
while ((nl = buffer.indexOf(`
`)) >= 0) {
const line = buffer.slice(0, nl).trim();
buffer = buffer.slice(nl + 1);
if (line !== "")
enqueue(line);
}
});
input.on("end", () => {
const tail = buffer.trim();
if (tail !== "")
enqueue(tail);
chain.then(() => resolve(), reject);
});
input.on("error", reject);
});
}
if (false) {}
// src/cli/commands/mcp.ts
function usage(message) {
return new CliError({
code: "USAGE",
exit: EXIT.USAGE,
message,
hint: "usage: kestrel mcp [--local | --api <url>] (see `kestrel mcp --help`)"
});
}
function resolveMcpTransport(args, api, env = process.env) {
const parsed = parseArgs(args, new Set(["local"]));
if (parsed.positionals.length > 0) {
throw usage(`unexpected \`mcp\` argument ${JSON.stringify(parsed.positionals[0])}`);
}
const local = parsed.bools.has("local");
if (local && api !== undefined) {
throw usage("`--local` contradicts `--api` — the MCP server speaks exactly one transport");
}
if (local)
return localTransport();
const raw = api ?? env.KESTREL_API;
const baseUrl = raw === undefined || raw === "" || raw === "default" ? DEFAULT_API : raw;
return remoteTransport({ baseUrl, fetch: globalThis.fetch });
}
async function mcpCommand(args, globals) {
const transport = resolveMcpTransport(args, globals.api);
await serveStdio(createKestrelMcpServer({ transport }));
return EXIT.OK;
}
export {
resolveMcpTransport,
mcpCommand
};