Skip to content

Add your own agent tools

View .md

Proof’s MCP server gives agents everything about documents: reading, writing, suggesting and commenting. It doesn’t know about the things your app keeps itself, such as folders, pins, tags, projects or due dates. To let agents use those too, register tools of your own with Proof. Its server lists them beside its own, at the same address, and forwards each call to your server, signed, saying which agent is calling and for whom.

By the end of this guide, an agent can file a document it just wrote into one of your folders, with nothing more for your users to connect.

Proof’s tools Yours
Address Your MCP address, https://mcp.proof.exalto.ai/acme The same
Tools Documents: read, write, suggest, comment Your features: folders, pins, tags
Runs on Proof Your server, called by Proof
Sign-in Proof’s, via your consent page The same: one approval covers both

Document ids are the same in both: doc_8Hq2Lm4R in Proof’s list_documents is the id your folders table already stores. So an agent can find a document with Proof’s tools and file it with yours, without any translation.

Keep document text in Proof’s tools. Your tools never write text into a document: words that don’t come through Proof’s own tools or editor aren’t credited to whoever wrote them.

  1. Describe them. Each tool is a name, a description and a JSON Schema for its arguments, as MCP describes tools. Names can’t be one of Proof’s own. Say in instructions how your tools relate to Proof’s, so agents use the two together.

    tools.ts
    export const TOOLS = [
    {
    name: "list_folders",
    title: "List folders",
    description: "The person's folders, in their order, with the documents in each.",
    inputSchema: { type: "object", properties: {}, additionalProperties: false },
    },
    {
    name: "move_document",
    title: "Move document",
    description: "File a document in one of the person's folders, or in none with folder_id null. Only when they ask.",
    inputSchema: {
    type: "object",
    properties: { document_id: { type: "string" }, folder_id: { type: ["string", "null"] } },
    required: ["document_id", "folder_id"],
    additionalProperties: false,
    },
    },
    ];
  2. Register them when your server starts, with the address Proof sends calls to and a secret of at least 32 characters, which you choose and keep:

    server.ts
    await proof.platform.setTools({
    url: "https://acme.com/proof/tools",
    secret: process.env.ACME_TOOLS_SECRET!,
    instructions: "Folders are the person's own: list_folders shows them, move_document files a document in one.",
    tools: TOOLS,
    });

    Each call replaces what you registered before, for the key’s mode. Agents see the change at their next tools/list.

  3. Answer the calls. Proof posts each call as JSON, signed in a Proof-Signature header as webhooks are:

    {
    "tool": "move_document",
    "arguments": { "document_id": "doc_8Hq2Lm4R", "folder_id": "fld_2" },
    "agent": { "id": "agt_3Vn8Qx2K", "name": "Claude Code", "access": "all" },
    "user": "usr_5Kq2Lm8R"
    }

    user is the Proof id of the person the agent acts for. Check the signature, run the tool for that person, and answer within 10 seconds with { "text": … }, what to tell the agent, and optionally data, given to it as structured content; or with { "error": … } to refuse.

    tools.ts
    import { verifySignature } from "@exalto/proof";
    app.post("/proof/tools", express.raw({ type: "application/json" }), async (req, res) => {
    if (!verifySignature(req.body, req.get("Proof-Signature"), process.env.ACME_TOOLS_SECRET!)) return res.sendStatus(401);
    const call = JSON.parse(String(req.body));
    const user = await db.users.findByProofId(call.user);
    if (!user) return res.json({ error: "That person has no account here." });
    switch (call.tool) {
    case "list_folders": {
    const folders = await db.folders.list(user.id);
    return res.json({ text: folders.map((f) => `- ${f.name} (folder_id: ${f.id})`).join("\n"), data: { folders } });
    }
    case "move_document": {
    const { document_id, folder_id } = call.arguments;
    const mine = await proof.users.listDocuments(call.user, { ids: [document_id] });
    if (!mine.data.length) return res.json({ error: `No document ${document_id} for this person.` });
    await db.folders.place(user.id, document_id, folder_id);
    return res.json({ text: "Moved." });
    }
    }
    res.json({ error: `Unknown tool ${call.tool}.` });
    });

    Check access against Proof, as move_document does, so your tools never reach a document the person can’t open. agent.access says whether the agent may reach all its person’s documents or only those chosen for it; refuse what an agent with chosen documents shouldn’t see, such as a list of every folder.

An answer that isn’t JSON, a status other than 2xx, or no answer in time reaches the agent as a failed call.

  • Platform: PUT /v1/platform/tools in the API reference.
  • MCP tools: the tools Proof’s server already has, so yours don’t repeat them.