Use your app from AI clients (MCP)
What you can do
Section titled “What you can do”You can make your app an MCP server (use your app from AI clients). MCP (Model Context Protocol) is a protocol that lets AI clients such as Claude and ChatGPT list and call external “tools”. For example, if you give an inventory app a “search items” tool and a “record an arrival” tool, users can operate the app from a conversation with their AI client, within their own permissions.
Your app only provides the tool declarations in the mcp: section of keelson.yaml and one JSON endpoint (handler) per tool. Keelson handles login, the consent screen, and the MCP exchange with the AI client.
- The app author declares tools in the
mcp:section ofkeelson.yamland deploys - A user registers the connection URL (the app URL +
/mcp) in their AI client - The first time, the Keelson login and consent screens appear. When the user approves, the AI client is connected as that user
- When the AI client calls a tool, Keelson checks membership, app permissions, and IP restrictions, and validates the arguments against the declaration
- Keelson calls
POST /api/mcp/<tool name>on your app and returns the JSON your app sent back to the AI client as the tool result
Only apps that run server-side code (apps with a command and no assets) can use this. Static sites, SPAs, and hybrid apps cannot declare it. It is available on every plan, with no differences between plans.
Declare tools
Section titled “Declare tools”Add an mcp: section to keelson.yaml and deploy. The example below declares a read tool, search_items, and a write tool, create_arrival.
slug: stock-deskruntime: python-slimcommand: "python app.py"db: mode: libsqlmcp: enabled: true instructions: "Look up stock levels and record incoming deliveries." tools: - name: search_items description: "Search stock by item name." access: read input: type: object properties: query: type: string required: [query] - name: create_arrival description: "Record one incoming delivery." access: write permission: manage input: type: object properties: item_id: type: string quantity: type: integer minimum: 1 required: [item_id, quantity]| Key | Meaning |
|---|---|
enabled | true publishes the tools. false keeps the declaration but stops calls |
instructions | A description of the tools as a whole, passed to the AI client (optional) |
tools[].name | Tool name. Starts with a lowercase letter and uses only lowercase letters, digits, and _. Your app’s endpoint is POST /api/mcp/<name> |
tools[].description | The tool description shown to the AI client. The AI reads it to decide when to call the tool |
tools[].access | read or write (changes data). See Read and write tools, permissions |
tools[].permission | view (default) or manage. A manage tool can be used only by people with manage permission on the app |
tools[].input | The shape of the arguments (JSON Schema 2020-12). The top level is type: object |
For the types, limits, and validation of each key, see the keelson.yaml reference.
Put many tools in a separate file (mcp.file)
Section titled “Put many tools in a separate file (mcp.file)”With mcp.file, you can put instructions and tools in a separate YAML file. keelson.yaml then contains only enabled and file.
slug: stock-deskruntime: python-slimcommand: "python app.py"db: mode: libsqlmcp: enabled: true file: mcp.yamlmcp.yaml in the same folder as keelson.yaml:
instructions: "Look up stock levels and record incoming deliveries."tools: - name: search_items description: "Search stock by item name." access: read input: type: object properties: query: type: string required: [query] - name: create_arrival description: "Record one incoming delivery." access: write permission: manage input: type: object properties: item_id: type: string quantity: type: integer minimum: 1 required: [item_id, quantity]- When you deploy, the Keelson CLI inserts the contents of
mcp.yamlintokeelson.yamlbefore validating and uploading it. Your local files are not rewritten - Only
instructionsandtoolscan go in the separate file.mcp.filecannot be combined withmcp.instructions/mcp.tools - Only newer versions of the Keelson CLI read
mcp.file. Older versions reject the deploy because of an unsupported key. If that happens, runkeelson upgradeto update the CLI to the latest version
Write the handlers
Section titled “Write the handlers”When a tool is called, Keelson sends POST /api/mcp/<tool name> to your app. The path is determined by the tool name and cannot be changed.
The body is the JSON object of arguments the AI client passed ({} when there are none). Only arguments that passed validation against the declared input reach your app.
Only headers that Keelson builds reach your app. No header sent by the AI client is passed through, so the AI client’s access token and forged X-Keelson-* headers never reach your app.
| Header | Content |
|---|---|
X-Keelson-User-Id | User ID of the connected user. Use this to identify the user |
X-Keelson-User-Email | The same user’s email address |
X-Keelson-User-Name | The same user’s display name |
X-Keelson-User-App-Perms | The user’s permissions on the app (view or view,manage) |
X-Keelson-Auth-Source | mcp (indicates a call through MCP) |
X-Keelson-Mcp-Tool | Name of the called tool |
X-Keelson-Mcp-Client | Identifier (URL) of the AI client |
X-Request-Id | Request ID. Also shown as request id: in error results |
The Authorization header carries a value used inside Keelson. It is not the AI client’s token, and your app does not use it. Use the user information headers the same way as in Use signed-in user information.
Return a response of 2xx with JSON (up to 1 MiB). Returning a JSON object makes the fields easier for the AI client to read.
| Your app’s response | Result returned to the AI client |
|---|---|
| 2xx with a JSON body | The JSON body as the tool result |
| 2xx with an empty body, a non-JSON body, or a body over 1 MiB | Error (invalid tool response) |
| 4xx | Error. The first 4 KiB of the body is shown as is to the user and the AI client |
| 3xx or 5xx | Error (tool execution failed). The body is not shown |
| No response within 60 seconds | Error (unknown whether it ran) |
- The body of a 4xx response is visible to the user and the AI client. Write only explanations that may be shown (for example, “Item A-100 was not found”)
- Each call has 60 seconds in total. Enqueue slow work as a background task and respond right away
- Keelson does not retry failed calls
FastAPI example
Section titled “FastAPI example”from fastapi import FastAPI, Requestfrom fastapi.responses import JSONResponse
app = FastAPI()
@app.post("/api/mcp/search_items")async def search_items(request: Request): user_id = request.headers["X-Keelson-User-Id"] args = await request.json() # e.g. {"query": "bolt"} items = find_items(owner=user_id, query=args["query"]) # your app's search return {"items": items}
@app.post("/api/mcp/create_arrival")async def create_arrival(request: Request): perms = request.headers.get("X-Keelson-User-App-Perms", "").split(",") if "manage" not in perms: return JSONResponse({"error": "You do not have permission to record arrivals"}, status_code=403) args = await request.json() if not item_exists(args["item_id"]): # The body of a 4xx response is visible to the user return JSONResponse({"error": f"Item {args['item_id']} was not found"}, status_code=404) arrival_id = record_arrival(args["item_id"], args["quantity"]) return {"arrival_id": arrival_id}Express example
Section titled “Express example”app.use(express.json());
app.post("/api/mcp/search_items", (req, res) => { const userId = req.headers["x-keelson-user-id"]; const items = findItems({ owner: userId, query: req.body.query }); // your app's search res.json({ items });});
app.post("/api/mcp/create_arrival", (req, res) => { const perms = (req.headers["x-keelson-user-app-perms"] ?? "").split(","); if (!perms.includes("manage")) { return res.status(403).json({ error: "You do not have permission to record arrivals" }); } const { item_id, quantity } = req.body; if (!itemExists(item_id)) { // The body of a 4xx response is visible to the user return res.status(404).json({ error: `Item ${item_id} was not found` }); } res.json({ arrival_id: recordArrival(item_id, quantity) });});Keelson does not pass calls to a permission: manage tool from people without manage permission. Checking X-Keelson-User-App-Perms in your app as well, as in the examples above, keeps the tool safe if the declaration changes.
Read and write tools, permissions
Section titled “Read and write tools, permissions”- Tools with
access: writeare shown separately on the consent screen as “Write tools (can change data)”. Users can choose “Approve read tools only”. In that case, write tools cannot be used. To use them later, the user connects again and approves them - Tools with
permission: managecan be used only by people with manage permission on the app. For assigning permissions, see Members and roles - Tools a user cannot use do not appear in the list. Tools that need permissions the user lacks, and write tools the user did not approve, are removed from the AI client’s tool list
- Every call is made as the connected user, within that user’s permissions. Calls from people removed from the workspace or who lost view permission on the app are usually rejected within 2 minutes
Connect
Section titled “Connect”Users register the connection URL in their AI client. The connection URL is the app URL followed by /mcp (for example, https://acme--stock-desk.keelson.run/mcp), and it is also shown on the app’s MCP tab in the console. For how to register the connection URL, see the MCP settings documentation of each AI client.
- Register the connection URL in the AI client
- The first time, the Keelson login screen opens. Log in with an account that can use the app
- On the consent screen, check the tools the AI client will use and select Approve
- Only members who can view the app can connect
- Only AI clients that identify themselves as
claude.aiorchatgpt.comare accepted. Other clients stop at the login step even if the connection URL is registered - Access tokens are valid for 1 hour, and the AI client renews them automatically. The renewal token expires after 60 days without use, or at most 180 days after connecting. When it expires, connect again
- Connecting the same AI client to the same app again disconnects the previous connection
Stop and disconnect
Section titled “Stop and disconnect”| Action | Who | Effect |
|---|---|---|
| Stop MCP for the workspace (Settings → Security → MCP (AI clients)) | Owner / Admin | Stops calls to every app in the workspace. Connections remain and work again once MCP is allowed |
Deploy with mcp.enabled: false | People who can deploy the app | Stops calls to that app. Connections remain and work again after you set it back to true and deploy |
| Revoke under “Connected AI clients” on the app’s MCP tab | Owner / Admin, people with manage permission on the app | Disconnects that connection. The AI client immediately loses access, including its renewal token |
| Revoke under “Your connected AI clients” | The user who connected | Disconnects the user’s own connection |
Stopping MCP and mcp.enabled: false only stop calls. Only disconnecting (revoking) ends a connection. “Your connected AI clients” is linked from Settings → Security and from the consent screen.
Reserved paths
Section titled “Reserved paths”- For an app with
mcp.enabled: true, Keelson responds to/mcp,/.well-known/oauth-protected-resource, and/.well-known/oauth-protected-resource/mcp, and they never reach your app - For apps that run server-side code (including the server side of a hybrid app), paths
/api/mcpand paths starting with/api/mcp/return 404 except through MCP, even without anmcp:declaration. Do not put routes other than tool handlers on these paths. Statically served parts are not affected
For the list of reserved paths, see Reserved URL paths in the Keelson Deploy Spec.
Limits
Section titled “Limits”| Item | Limit |
|---|---|
| Number of tools | 40 per app |
instructions | 4096 characters |
Tool description | 1024 characters |
Tool input | 64 KB as JSON, 32 levels of nesting |
| One call | 60 seconds |
| Response body (2xx) | 1 MiB |
| Calls | 60 per minute per connection |
| Concurrent calls | 10 per app (per Keelson gateway instance; when the gateway scales out, your app can receive more concurrent calls than this in total) |
Calls over the rate or concurrency limit are returned to the AI client with a request to retry after a short wait. Limits do not change by plan. The concurrency limit of 10 is not a ceiling on the concurrent calls your app receives, so do not size database connection pools or similar resources on the assumption of 10.
Troubleshooting
Section titled “Troubleshooting”| Symptom | What to check |
|---|---|
| A tool does not appear in the list | Whether the user has permission on the app (permission: manage tools need manage permission). For a write tool, whether the user approved it on the consent screen. Whether mcp.enabled is true. Whether MCP is stopped for the workspace (shown in the status on the app’s MCP tab) |
| Calls are rejected in a workspace with IP restrictions | IP restrictions also apply to calls through MCP. Calls from claude.ai or ChatGPT come from each provider’s servers, not from the user’s computer, so they are not allowed as is. Review the access sources in IP restrictions |
| ”Invalid tool response” is shown | Whether the handler returns 2xx with a JSON body, and the body is not empty, plain text, or over 1 MiB |
| Cannot connect after logging in | Whether the AI client identifies itself as claude.ai or chatgpt.com. Whether the account you logged in with is a member who can view the app |
Deploy rejects file under mcp as an unsupported key | The Keelson CLI is old. Run keelson upgrade to update to the latest version, then deploy again |