Skip to content
Prism

Surfaces

One operation, four projections — and each trust boundary.

Same router. Four projections. Each states who it trusts.

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.

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) => Response

Unauthenticated 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.

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.

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.

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.

@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" });