Surfaces
One operation, four projections — and each trust boundary.
Same router. Four projections. Each states who it trusts.
Field casing
Section titled “Field casing”Every projection accepts fieldCase:
| Surface | Default | Why |
|---|---|---|
toFetch / toCli |
"preserve" |
Human / operator wire names match the contract |
toMCP / toAiSdkTools |
"snake" |
Agent tools conventionally use snake_case |
Input and output use the same rule. Path params in ~x/http.path stay as written.
REST + OpenAPI — remote-safe
Section titled “REST + OpenAPI — remote-safe”Trusted when you deploy the server and hold credentials. Safe to expose on the public internet behind verify.
import { agent, http } from "@prism/agent";import { implement, op } from "@prism/core";import { toFetch } from "@prism/hono";import { z } from "zod";
const ping = implement( op() .input(z.object({ name: z.string() })) .output(z.object({ message: z.string() })) .meta( http({ method: "POST", path: "/ping" }), agent({ description: "Say hello." }), ),).handler(async ({ input }) => ({ message: `hello ${input.name}` }));
const app = toFetch( { ping }, { openapi: { title: "Demo", version: "0.1.0" }, fieldCase: "preserve", // verify: credentialVerifier, // required for authenticated routes },);
void app; // Fetch-compatible (Request) => ResponseUnauthenticated calls when verify is set return 401 with problem type AUTHENTICATION_REQUIRED (“Authentication required”) and WWW-Authenticate: Bearer. When you configure a resource metadata URL, the challenge adds resource_metadata.
MCP — remote-safe with verify
Section titled “MCP — remote-safe with verify”toMCP builds an MCP server. For HTTP, use MCP SDK v2 (@modelcontextprotocol/{core,server,hono} — the v1 @modelcontextprotocol/sdk package is gone):
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/server";import { createMcpHonoApp } from "@modelcontextprotocol/hono";import { agent, deniedPrincipal, http } from "@prism/agent";import { implement, op } from "@prism/core";import { toMCP } from "@prism/mcp";import { z } from "zod";
const ping = implement( op() .input(z.object({ name: z.string() })) .output(z.object({ message: z.string() })) .meta( http({ method: "POST", path: "/ping" }), agent({ description: "Say hello." }), ),).handler(async ({ input }) => ({ message: `hello ${input.name}` }));
const router = { ping };const mcp = toMCP(router, { principal: deniedPrincipal(), fieldCase: "snake", // Prefer verify + credentialTokenStorage for HTTP hosts});
const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined, // stateless});const mcpHttp = createMcpHonoApp();
void mcp;void transport;void mcpHttp;Trust boundary: a locally spawned MCP process (e.g. Claude Desktop) is only safe when the operator holds every embedded credential — not something to distribute to end users. Streamable HTTP with verify is the remote-safe path.
AI SDK tools — trusted process only
Section titled “AI SDK tools — trusted process only”Tools run in the caller’s process. Safe in a server-side route handler. Unsafe inside a desktop or browser app with embedded credentials.
import { agent } from "@prism/agent";import { deniedPrincipal } from "@prism/agent";import { implement, op } from "@prism/core";import { toAiSdkTools } from "@prism/ai-sdk";import { z } from "zod";
const ping = implement( op() .input(z.object({ name: z.string() })) .output(z.object({ message: z.string() })) .meta(agent({ description: "Say hello." })),).handler(async ({ input }) => ({ message: `hello ${input.name}` }));
const tools = toAiSdkTools( { ping }, { principal: deniedPrincipal(), fieldCase: "snake", },);
void tools;Streaming ops (op().stream()) project to an async-generator execute that yields preliminary tool results; REST/MCP/CLI collapse to the final value.
CLI — trusted operator
Section titled “CLI — trusted operator”import { agent, http } from "@prism/agent";import { implement, op } from "@prism/core";import { toCli } from "@prism/cli";import { z } from "zod";
const ping = implement( op() .input(z.object({ name: z.string() })) .output(z.object({ message: z.string() })) .meta( http({ method: "POST", path: "/ping" }), agent({ description: "Say hello." }), ),).handler(async ({ input }) => ({ message: `hello ${input.name}` }));
const run = toCli( { ping }, { name: "demo", fieldCase: "preserve" },);
void run(["ping", "--name", "prism"]);Assumes a human at the keyboard (confirm prompts, local credentials). Not a remote attack surface by itself — pair it with HTTP transport when talking to a server.
Typed client
Section titled “Typed client”@prism/client accepts a handler-free contracts tree (or a full router). It never calls handlers — safe to ship contracts to a browser.
import { agent, http } from "@prism/agent";import { op, resolveContract } from "@prism/core";import { createClient } from "@prism/client";import { z } from "zod";
const pingContract = resolveContract( op() .input(z.object({ name: z.string() })) .output(z.object({ message: z.string() })) .meta( http({ method: "POST", path: "/ping" }), agent({ description: "Say hello." }), ),);
const routes = { ping: { contract: pingContract } };
const client = createClient(routes, { baseUrl: "http://localhost:3000", token: "…",});
void client.ping({ name: "prism" });