{"slug": "we-put-an-mcp-endpoint-in-49-business-apps-here-is-what-a-read-only-key-can-and", "title": "We put an MCP endpoint in 49 business apps. Here is what a read-only key can and cannot do to an invoice register.", "summary": "A developer building self-hosted business tools implemented Model Context Protocol (MCP) endpoints across 49 applications, focusing on what a read-only API key can and cannot do to a live invoice register. The key design choice is to refuse writes at the advertisement layer: read-only keys stop advertising write tools in the tools/list response, so models cannot be tricked into using them. The key acts as a single user inheriting role permissions, and all actions are logged in the audit trail with the key's user identity, while destructive and outbound email actions remain human-only.", "body_md": "We build small self-hosted business tools, and since our 3.0 release every one of them except our AI client answers the Model Context Protocol at `POST /mcp`\n\n. Forty-nine of them. That was a large enough change, applied uniformly enough, that the interesting engineering question stopped being \"how do we add MCP\" and became \"what should a language model be allowed to do to a live invoice register.\"\n\nThis post is about the second question, because it is the one that actually matters and the one most MCP integrations answer by accident.\n\nThere is nothing vendor-specific in it. Three facts:\n\n`https://your-install/mcp`\n\n- your server, your domain.`Authorization: Bearer apk_...`\n\nheader.That is the whole contract. In Claude it is one CLI line:\n\n```\nclaude mcp add --transport http invora https://your-install/mcp \\\n  --header \"Authorization: Bearer apk_xxxx\"\n```\n\nIn the OpenAI Responses API it is one entry in the `tools`\n\narray:\n\n```\n{\n  \"type\": \"mcp\",\n  \"server_label\": \"invora\",\n  \"server_url\": \"https://your-install/mcp\",\n  \"authorization\": \"apk_xxxx\",\n  \"require_approval\": \"never\"\n}\n```\n\nClients that keep servers in a config file take the same three fields under a different set of key names. n8n's MCP Client node takes the URL and the same Authorization header. And if you would rather not use a client at all, it is plain JSON-RPC 2.0 over one POST:\n\n```\ncurl -X POST https://your-install/mcp \\\n  -H \"Authorization: Bearer apk_xxxx\" -H \"Content-Type: application/json\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}'\n```\n\nWe implemented the protocol rather than an integration with a particular vendor, which means clients that do not exist yet will work too. That is the main argument for MCP over building N bespoke connectors, and it is a good one, but it is not what this post is about.\n\nOnce your invoice register speaks a protocol that a model can drive, you have handed a probabilistic system a set of verbs against your real data. The usual answer is a scope string in a token and a hope that the model behaves. We did not love that, for a specific reason: a model that has been told about a tool will eventually try to use it. The instruction not to is a suggestion. The absence of the tool is not.\n\nSo the read-only key does not refuse writes at the permission layer. It refuses them at the advertisement layer.\n\nWhen you mint a key you choose what it may do. Pick **read only**, and the endpoint stops advertising the tools that change anything. They are not in the `tools/list`\n\nresponse. From the model's point of view they do not exist, so there is no capability to be talked into using, no jailbreak that unlocks an option that was never on the menu, and no confused-deputy path where a cleverly worded invoice description persuades an assistant to mark itself paid. If something does reach for a write tool anyway, it is refused by name.\n\nTwo other properties, both of which we consider more important than the feature list:\n\n**The key acts as one user and inherits that user's role.** It is not a superuser channel that runs beside your permission model. It is a user. If that user cannot see a record in the browser, the key cannot see it through MCP either. This is the single biggest thing people get wrong when bolting an API onto an app that already has roles: they build a second, parallel authorisation system, and then maintain two of them badly.\n\n**Everything a read-and-write key does lands in the audit log with the key's user beside it.** Not \"an API call happened\", but which user the key acts as. When an assistant drafts an invoice at 2 a.m., that is attributable in the same place and the same format as a human doing it at 2 p.m.\n\nThis is the constraint we are most confident about, and it is a product decision rather than a technical one: **no tool in the catalogue deletes your records, and none emails your customers.** Destruction and outbound mail stay human clicks, deliberately.\n\nBoth halves of that are chosen for the same reason. Deletion is the operation you cannot review after the fact, and outbound email is the operation whose blast radius is other people. Everything else an assistant does to a business record is recoverable by looking at it and changing it back. An email that has gone to your client list is not, and a deleted row that nobody noticed for a month is not either. So those two verbs do not exist in the tool surface at all, on any key, at any permission level.\n\nThat means there are things you cannot ask our tools to do, and we think that is correct. \"Clean up the old invoices\" is not a request we want to be able to fulfil.\n\nThe exact tool list differs per product and lives in an `API.md`\n\ninside each download rather than in one central schema, which is fine when you are driving one product and less elegant when you are driving six.\n\n`require_approval: \"never\"`\n\nappears in the ChatGPT snippet above because it is what makes the thing pleasant to use, and you should think about whether you want that on a write-capable key. We would not.\n\nAnd read-only is a real boundary, not a security guarantee against a determined operator: whoever can sign in as that user can mint another key with different permissions from the same settings page. The boundary protects you from the model, not from the person.\n\nThe version of this that matters is not \"ask your AI about your invoices\". It is that a small business running six self-hosted tools now has a uniform way to query all six without any of that data leaving their server, because the model can run on their machine too. Nothing in the recipe requires a hosted model. The endpoint does not care what is on the other end of it.\n\nThat is a fairly old idea - your data, your server, your software - with one new interface bolted on where it turns out to fit well.\n\nWe build Ownware, the catalogue these endpoints ship in: [https://ownware.io/connect-your-ai](https://ownware.io/connect-your-ai) has the recipe for every client. This post was written by a human with AI assistance; every technical claim was checked against that page on the day of posting.", "url": "https://wpnews.pro/news/we-put-an-mcp-endpoint-in-49-business-apps-here-is-what-a-read-only-key-can-and", "canonical_source": "https://dev.to/ownware_io/we-put-an-mcp-endpoint-in-49-business-apps-here-is-what-a-read-only-key-can-and-cannot-do-to-an-4l38", "published_at": "2026-08-30 18:22:18+00:00", "updated_at": "2026-08-30 18:53:03.063010+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-infrastructure", "ai-safety"], "entities": ["Model Context Protocol", "Claude", "OpenAI", "n8n"], "alternates": {"html": "https://wpnews.pro/news/we-put-an-mcp-endpoint-in-49-business-apps-here-is-what-a-read-only-key-can-and", "markdown": "https://wpnews.pro/news/we-put-an-mcp-endpoint-in-49-business-apps-here-is-what-a-read-only-key-can-and.md", "text": "https://wpnews.pro/news/we-put-an-mcp-endpoint-in-49-business-apps-here-is-what-a-read-only-key-can-and.txt", "jsonld": "https://wpnews.pro/news/we-put-an-mcp-endpoint-in-49-business-apps-here-is-what-a-read-only-key-can-and.jsonld"}}