kolu
Docs

How to expose a surface to agents

An agent (Claude Code, Codex, opencode) drives a surface through the Model Context Protocol: your cells and streams become readable resources, your procedures become callable tools. @kolu/surface-mcp does the projection; you decide what to expose.

1. Install and import

{ "dependencies": { "@kolu/surface-mcp": "workspace:*" } }
import { implementSurface } from "@kolu/surface/server";
import { directLink } from "@kolu/surface/links/direct";
import { serveSurfaceAsMcp } from "@kolu/surface-mcp";

2. Build a client for the surface

Choose one of two shapes:

  • Serve fresh when the MCP server is the backend — wrap an in-process implementSurface in a directLink.
  • Bridge a live surface when a server is already running — dial it over its socket or ssh stdio instead.
const { router } = implementSurface(surface, {
  channel: inMemoryChannelByName(),
  cells: { load: { store: inMemoryStore(ZERO) } },
  collections: {
    processes: {
      readAll: () => table,
      upsert: (pid, proc) => {
        table.set(pid, proc);
      },
      remove: (pid) => {
        table.delete(pid);
      },
    },
  },
  streams: {
    nodeLog: {
      source: async function* (nodeId) {
        yield { kind: "snapshot", text: `opened ${nodeId}`, done: false };
      },
    },
  },
  events: { autosave: {} },
  procedures: {
    proc: {
      kill: async ({ input, ctx }) => {
        ctx.collections.processes.remove(input.pid);
        return { ok: true };
      },
    },
  },
});
const client = directLink<typeof surface.contract>(router); // serve-fresh

3. Name what to expose

expose is default-deny: an unlisted member never reaches the agent. Map each key to "resource" (cells, collections, streams, events) or a tool (procedures).

await serveSurfaceAsMcp({
  surface,
  client: () => client,
  expose: {
    load: "resource", // cell   → readable, subscribable
    nodeLog: "resource", // stream → readable, subscribable
    "proc.kill": { tool: { mutates: true } }, // procedure → mutating tool
    // "proc.configure" omitted → never reaches the agent
  },
});

4. Curate a dangerous surface (optional)

If the surface has procedures no agent should reach, do not rely on omission alone — project a safe view with projectSurface (from @kolu/surface/project): drop the dangerous procedures, bound the logs, derive read-only verdicts, and expose that second surface. It is the difference between hiding a control and not wiring it at all.

5. Point the agent at it

The server speaks stdio by default. Register it in the agent’s .mcp.json and the tools and resources appear in its next session. For the full option list and the resource-URI scheme, see the @kolu/surface-mcp reference.