Custom MCP servers
Bring internal systems into the paper with a remote MCP server: collect-only tools, authentication, news hints and a TypeScript example.
If a system isn’t in our catalog (an internal admin panel, a data warehouse, a homegrown CRM), expose it as a remote Model Context Protocol server and Paperbeam will read from it. You decide exactly which tools Paperbeam may call. Available on Growth, Business and Enterprise.
How Paperbeam uses your server
- Paperbeam connects as an MCP client over Streamable HTTP (falling back to SSE) and calls
tools/list. - You classify every tool as Collect or Blocked. Only collect tools are ever called, only while an edition is collecting, and never by a model: the pipeline calls them directly and treats the results as data.
- Each collect tool is called once per edition, within a budget of 10 tool calls and a 20-second timeout per call.
- Results become items that go through the same extraction, budgeting and verification as every other source.
Connect a server
- Go to Sources›Custom MCP server.
- Enter the server’s https URL, for example
https://mcp.acme.com/mcp. - Choose authentication:
- Bearer token: sent as
Authorization: Bearer <token>. - Custom header: sent as
<Header-Name>: <value>, e.g.X-API-Key. If you name the headerAuthorization, the value is sent as-is, e.g.Token abc123. - None: for servers on a private allow-listed network.
- Bearer token: sent as
- Click List tools. Paperbeam suggests a classification for each tool. Tools that declare
readOnlyHint: trueare suggested as Collect. Tools whose name or description suggests a write (create, update, delete, send, run…) are suggested as Blocked. Review every suggestion: you have the final say. - Add a news hint to each collect tool (up to 200 characters), for example “Signed partner agreements” or “Daily active users by plan”. It tells the extractor what the data represents and shapes the item’s kind.
- Name the connection and click Connect server. You can reclassify tools and edit hints later on the source page.
Designing collect tools
A good collect tool answers the question “what happened recently that people should know?”
- Take no required arguments. Paperbeam calls collect tools with an empty argument object. Default to a recent window (for example the last 7 days); Paperbeam keeps only records dated inside the edition’s window.
- Return events, not state. “Contracts signed”, not “all contracts”.
- Return JSON. Either
structuredContent, or a text content block containing JSON. Plain text works too, and is read as a single document. - Mark it read-only with
annotations.readOnlyHint: true. Name it with a read verb first, likelist_signed_contracts. - Keep it fast. Each call has a 20-second timeout.
Result format
Return an array of records, or an object with the array under one of items, results, data, records, rows, events or deals. A single object is treated as one record. Paperbeam recognizes these fields (first match wins):
| Meaning | Field names | Notes |
|---|---|---|
| Title | title, name, subject, headline, summary | Up to 300 characters |
| Body | body, description, summary, text, content, details | Up to 4,000 characters |
| When | occurred_at, occurredAt, date, created_at, createdAt, timestamp, updated_at, updatedAt, closed_at, closedAt | ISO 8601. Records outside the window are dropped; undated records count as today. |
| Amount | amount, value, arr, mrr | Numbers only |
| Link | url, link, permalink, html_url, href | http(s) only. Shown as the story’s source link. |
| Id | id, uuid, key, identifier | Stable ids prevent duplicates across editions |
| Kind | type, kind | e.g. deal, expansion, churn, ticket, release, metric, hire. Otherwise inferred from the news hint and tool name. |
Other scalar fields (up to 40) are kept as structured data the extractor can use.
Example: a TypeScript MCP server
A minimal server with one collect tool, built with the official MCP TypeScript SDK and served over Streamable HTTP with a bearer token.
npm init -y && npm pkg set type=module
npm install @modelcontextprotocol/sdk express zod
npm install -D tsx @types/expressimport express from "express";
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const TOKEN = process.env.PAPERBEAM_MCP_TOKEN!; // a long random string you paste into Paperbeam
// Replace with a query against your own system (read-only credentials!).
async function signedContractsSince(since: Date) {
return [
{
id: "ctr_1042",
type: "deal",
title: "Globex signs a 3-year enterprise agreement",
body: "Globex Freight signed for 400 seats after a six-week pilot with the logistics team.",
customer: "Globex Freight",
amount: 186000,
occurred_at: new Date().toISOString(),
url: "https://contracts.acme.internal/ctr_1042",
},
].filter((c) => new Date(c.occurred_at) >= since);
}
function buildServer() {
const server = new McpServer({ name: "acme-contracts", version: "1.0.0" });
server.registerTool(
"list_signed_contracts",
{
title: "Signed contracts",
description: "Contracts signed in the last N days (default 7). Read-only.",
inputSchema: { days: z.number().int().min(1).max(31).default(7) },
annotations: { readOnlyHint: true, openWorldHint: false },
},
async ({ days }) => {
const since = new Date(Date.now() - days * 86_400_000);
const items = await signedContractsSince(since);
return {
structuredContent: { items },
content: [{ type: "text", text: JSON.stringify({ items }) }],
};
},
);
return server;
}
const app = express();
app.use(express.json());
// Bearer auth for every MCP request.
app.use("/mcp", (req, res, next) => {
if (req.headers.authorization !== `Bearer ${TOKEN}`) return res.status(401).json({ error: "unauthorized" });
next();
});
// Stateless Streamable HTTP: a fresh server + transport per request.
app.post("/mcp", async (req, res) => {
const server = buildServer();
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
res.on("close", () => {
transport.close();
server.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.get("/mcp", (_req, res) => res.status(405).end());
app.delete("/mcp", (_req, res) => res.status(405).end());
app.listen(Number(process.env.PORT ?? 8787), () => console.log("MCP server on :8787/mcp"));PAPERBEAM_MCP_TOKEN=$(openssl rand -hex 32) npx tsx server.tsDeploy it behind HTTPS (any host works), then in Paperbeam add https://your-host/mcp with Bearer token auth. list_signed_contracts will be suggested as Collect. Give it the news hint “Signed customer contracts”, and tomorrow’s paper can lead with Globex.
Test it before connecting
npx @modelcontextprotocol/inspector
# Transport: Streamable HTTP · URL: http://localhost:8787/mcp
# Header: Authorization: Bearer <your token> → List Tools → Run list_signed_contractsTroubleshooting
| Symptom | Fix |
|---|---|
| “MCP servers must use https.” | Serve the endpoint over TLS. Plain http is refused in production. |
| List tools fails | Check the URL path (often /mcp or /sse), the token, and that the server answers within 15 seconds. |
| Tool “x” is not offered by the server; skipped | The tool was renamed or removed. Reclassify tools on the source page. |
| Tool-call budget exhausted | More than 10 collect tools. Consolidate them, or block the less important ones. |
| Nothing from the server appears in the paper | Check that records have dates inside the edition window, and that their content is newsworthy. Routine state rarely becomes a story. |