Skip to main content

MCP

Looking for the product view?
The Help Center explains this surface as users see it: MCP.

An App with an mcp/ directory is a Model Context Protocol server. External MCP clients (Claude Code, Claude Desktop, Cursor) connect to a per-App endpoint, authenticate as an Informer user, and call the App's exposed tools.

Key Concepts​

The mcp/ directory is the exposure boundary. Tool files there share the file contract and sandbox of tools/ (an exported description, a JSON Schema schema, and a handler), but only mcp/ tools are reachable by MCP clients. Tools in tools/ remain callable by the App's agents only. Both live in one namespace, so a name may not appear in both directories; deploy fails if it does. An agent may reference an mcp/ tool by name, but never the reverse.

Two optional exports describe the tool beyond its schema — one for external clients, one for chat:

// mcp/create_ticket.js
export const description = 'Create a ticket on a board';
export const schema = { properties: { title: { type: 'string' } } };

// MCP-standard hints, published to external clients in tools/list
export const annotations = { readOnlyHint: false, destructiveHint: false };

// Explicit human-in-the-loop request, honored by Informer GO chat
export const needsApproval = true;

export async function handler({ args }) {
return { created: args.title };
}

The two are independent. annotations describes the tool to external MCP clients; needsApproval is the only thing chat reads to decide whether to ask.

needsApproval must be a const boolean literal — true or false, nothing else. It is never evaluated, because an expression that happens to be truthy at deploy time is not a promise about runtime. An expression (export const needsApproval = IS_PROD), a let/var, or an export form the scanner cannot read the value from (export { needsApproval }) each report a deploy warning and publish the strictest reading, leaving the tool asking before every call until it is fixed. Declaring nothing is a third state, distinct from declaring false: see the chat behavior below.

annotations must be an object literal at the =; its contents are evaluated in a sandboxed isolate with no host globals, so a reference to anything outside the literal warns and publishes nothing. Keys outside the MCP-standard set (title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint), and standard keys carrying the wrong type, warn at deploy — the object is republished to clients verbatim, so a typo is otherwise invisible. Because the object is evaluated rather than read as a literal, readOnlyHint is not an approval input: a tool that only annotates itself read-only is still confirmed on its first chat call. Use needsApproval = false to vouch for it.

Identity is the calling user, not the App owner. tools/call executes the tool bundle with the caller's credentials, so dependency slots declared runAs: user resolve to them and their row level security applies. The caller's App share roles are resolved and passed to the handler on run.roles, and run.trigger is mcp.

Access is share-based, exactly like the App view surface: the endpoint resolves the App through the read_access scope, so a user the App is not shared with receives 404 rather than 403.

Chats can attach an App's tools directly. An Informer GO chat lists Apps that expose mcp/ tools and attaches them as context (appIds on the chat). The tools register on the model call and execute through the same sandbox path with the chatting user's identity and share roles, and run.trigger is chat. No MCP client or Toolkit is involved. The chat's AI can also find an App on its own: searchResources lists readable Apps that expose mcp/ tools alongside Datasets, Toolkits, and Assistants, and addResource proposes one to the user, who approves before it joins the chat.

Surfaces without a persisted chat can attach them too, by sending appIds on each POST /api/models/{id}/_chat request — an App's own AI panel, or the copilot. Ids may be a uuid or an ownerId:slug naturalId, are resolved through the same share-based read access, and one the caller cannot read is dropped rather than failing the request. Two differences from a persisted chat follow from there being no chat row:

  • The AI does not offer Apps on these surfaces. addResource attaches by writing the chat, so there is nothing for it to write; the resource catalog omits Apps rather than proposing one it could not add. The user picks the Apps, and their client sends the ids.
  • A caller must answer approval itself. A tool that declares nothing asks on its first call — the common case, not an edge case — and the approval arrives as a tool part awaiting a response. A client with no approval affordance leaves every gated tool as a call that never returns.

Because there is no chat library to offload into, an oversized tool result is truncated in place on these surfaces, with the truncation stated in the result rather than silently shortening it.

Chat asks before running a tool the App has not vouched for. Each mcp/ tool lands in one of three approval behaviors, and the user's answer is rendered by the same approval card a Toolkit tool's needsApproval produces:

DeclarationChat behavior
needsApproval = trueAsks on every call
needsApproval = falseNever asks
Not declared (annotations alone does not count)Asks on the first call; once the user lets it through, later calls in that conversation run without asking. A denial is not remembered, so the next call asks again

A denied call reaches the model as a failed tool result, not as a silent no-op. The remembered approval lives in the conversation, so a tool approved in an earlier turn stays approved until it falls out of the model's context window. Approval also carries across later steps of one turn, though several calls the model issues in the same step are each confirmed.

Invocations are recorded in app_tool_invocation with a null agentId and a trigger of mcp (external MCP clients) or chat (chat-attached tools), which is what distinguishes this traffic from agent runs in App stats. Preparation failures (a missing bundle, role resolution) are recorded too, so a tool that never reached the sandbox still leaves a failed row.

MCP API Endpoints​

POST /api/apps/{id}/mcp​

The MCP endpoint, implementing the streamable HTTP transport in stateless JSON mode: one JSON-RPC 2.0 message per request, no sessions and no SSE stream.

Authentication: Required. Any strategy the API accepts works, including an API token or an OAuth access token bound to this App.

Permissions Required: None beyond read access to the App (the App must be shared with the caller).

Request headers: Accept must include both application/json and text/event-stream, and Content-Type must be application/json. The transport answers 406 and 415 respectively when they are missing.

Supported methods: initialize, notifications/initialized, tools/list, tools/call.

tools/list is served from deployed tool metadata and returns every tool whose source lives in mcp/, each with its description and inputSchema, plus its annotations when the tool declares them (the key is absent otherwise, rather than an empty object asserting hints the author never made). tools/call loads the tool's deploy-time bundle and runs it in the App sandbox. A tool that throws returns a result with isError: true rather than a JSON-RPC error, so the client's model can read the failure; an unknown tool name is a JSON-RPC -32602 (invalid params).

Responses: 200 with the JSON-RPC response, 202 for notifications, 404 when the App is not readable by the caller, 401 when unauthenticated.

A 401 carries a WWW-Authenticate: Bearer resource_metadata="..." header pointing at this App's protected resource metadata, which is how a client with only the endpoint address discovers where to sign in.


GET|DELETE /api/apps/{id}/mcp​

Response: 405 with an Allow: POST header.

The MCP specification expects these verbs to be refused by a server that offers neither a standalone SSE stream (GET) nor session termination (DELETE). The App lookup still runs first, so a user the App is not shared with gets 404 rather than a 405 that would confirm the App exists.


GET /api/mcp-apps​

Lists the Apps visible to the caller that expose mcp/ tools, as a flat array. This backs the Apps context picker in Informer GO chats.

Authentication: Required

Permissions Required: None; results are filtered by the App read_access scope, and draft Apps are excluded.

Response: an array of { id, naturalId, name, description, tools } ordered by name, where tools carries each mcp/ tool's name and description. Linked as inf:mcp-apps in the API index.


The endpoint address​

The address is linked as inf:app-mcp on the App resource and in the API index, so clients and UI resolve it by rel rather than building it. It accepts the App's uuid or its natural id; the uuid form survives a rename.