Forwarding identity to MCP servers
This property is specific to AnythingLLM only and will have no effect on other tools.
By default, every MCP server in AnythingLLM runs as one shared connection for the whole instance. Every user and every workspace calls your MCP server with the same static headers from the config file, so the server has no idea who is on the other end.
Identity forwarding fixes that. When enabled for a server, AnythingLLM tells the MCP server which user and which workspace the agent is acting for on every request. Your MCP server can then:
- Show different tools to different users or roles (filter
tools/list). - Refuse a tool call the caller is not allowed to make.
- Scope data per user or per tenant. For example, one workspace per customer, with the MCP server connecting to that customer's database.
This is identity forwarding, not authentication. Your MCP server trusts whatever identity AnythingLLM sends, so it should only accept requests from your AnythingLLM instance. Keep a shared secret in the static headers block (like X-API-KEY) and/or keep the MCP server on a private network.
Enabling it
Add forwardIdentity to the anythingllm block of the MCP server in your anythingllm_mcp_servers.json file. It is off by default and is set per server.
{
"mcpServers": {
"my-tenant-mcp": {
"type": "streamable",
"url": "http://my-mcp:8000/mcp",
"headers": {
"X-API-KEY": "shared-secret-between-anythingllm-and-my-mcp"
},
"anythingllm": {
"forwardIdentity": true
}
}
}
}forwardIdentity is either true (forward everything) or a list of the fields you want. Only send what your MCP server actually uses.
| Field | Value | Header |
|---|---|---|
user.id | The user's ID, e.g. 12 | X-AnythingLLM-User-Id |
user.username | The username, e.g. jane.doe | X-AnythingLLM-Username |
user.role | admin, manager or default | X-AnythingLLM-User-Role |
workspace.id | The workspace ID, e.g. 7 | X-AnythingLLM-Workspace-Id |
workspace.slug | The workspace slug, e.g. tenant-acme | X-AnythingLLM-Workspace-Slug |
"user" and "workspace" are shorthands for all of their fields:
| Value | What is forwarded |
|---|---|
true | Every field |
["user", "workspace"] | Every field |
["user"] | user.id, user.username, user.role |
["workspace"] | workspace.id, workspace.slug |
["user.id", "workspace.slug"] | Only the user ID and the workspace slug |
["user.role"] | Only the user's role |
false or not set | Nothing (default) |
Anything that is not one of the fields or shorthands above is ignored.
After editing the file, click Refresh on the "Agent Skills" page to reload your MCP servers.
What your MCP server receives
Identity is sent in two places so it works on every transport:
- HTTP headers. For
sseandstreamableservers only. - The request's
_metafield, under the keycom.anythingllm/identity. For all servers, includingstdio._metais the field the MCP protocol provides for metadata on a request.
Headers
Each forwarded field is sent as its own header, listed in the field table above. Any header value that is not plain ASCII (for example a workspace slug with non-latin characters) is URL-encoded.
_meta
The same fields, grouped by user and workspace. This example forwards every field:
{
"method": "tools/call",
"params": {
"name": "get_invoices",
"arguments": { "month": "2026-10" },
"_meta": {
"com.anythingllm/identity": {
"user": { "id": 12, "username": "jane.doe", "role": "default" },
"workspace": { "id": 7, "slug": "tenant-acme" }
}
}
}
}The same _meta is sent on tools/list. With "forwardIdentity": ["user.id", "workspace.slug"] it would only contain { "user": { "id": 12 }, "workspace": { "slug": "tenant-acme" } }.
Which requests carry identity
| Request | Identity sent? |
|---|---|
tools/list when an agent session starts | ✅ So you can return a different tool list per user |
tools/call from an agent | ✅ Every call |
initialize and connection setup | ❌ The connection is shared by everyone |
tools/list from the "Agent Skills" admin page | ❌ No agent session, so no identity |
Because the "Agent Skills" page lists tools without an identity, your server should decide what an anonymous tools/list returns. Returning the full list lets admins see and toggle every tool in the UI. Either way, always check permissions on tools/call as well. An agent can try to call a tool by name even if it was never listed for that user.
When each value is available
Values that don't exist for a request are left out entirely. AnythingLLM never sends an empty header or a null placeholder. If none of the requested fields has a value, no identity headers and no _meta are sent at all.
| Where the agent runs | User | Workspace |
|---|---|---|
| Workspace chat, multi-user mode | ✅ | ✅ |
| Workspace chat, single-user mode | ❌ No users exist | ✅ |
| AnythingLLM Desktop | ❌ No users exist | ✅ |
| Mobile app | ✅ In multi-user mode | ✅ |
Developer API: thread chat with userId | ✅ In multi-user mode | ✅ |
Developer API: workspace chat, or thread chat without userId | ❌ | ✅ |
| Telegram bot | ❌ | ✅ |
| Scheduled jobs | ❌ | ❌ |
On the developer API, the user is whatever userId the API caller passes. Anyone holding an AnythingLLM API key can act as any user, so treat API keys as having full access when identity forwarding is on.
Reserved header prefix
On a server with forwardIdentity enabled, all headers starting with X-AnythingLLM- are reserved for identity forwarding. If that server's static headers block contains one, AnythingLLM ignores it and logs a warning. This way a hardcoded value in the config can never be mistaken for a real forwarded identity.
Servers without forwardIdentity are not affected. Their headers are sent exactly as written, including any X-AnythingLLM- headers.
Example: checking identity in your MCP server
A minimal Node.js server using the official MCP TypeScript SDK (opens in a new tab). The same idea works in any language. Read the headers, or params._meta["com.anythingllm/identity"] if you also want to support stdio.
const {
ListToolsRequestSchema,
CallToolRequestSchema,
} = require("@modelcontextprotocol/sdk/types.js");
function callerOf(request, extra) {
const headers = extra?.requestInfo?.headers ?? {};
const meta = request.params?._meta?.["com.anythingllm/identity"];
return {
username: headers["x-anythingllm-username"] ?? meta?.user?.username,
role: headers["x-anythingllm-user-role"] ?? meta?.user?.role,
workspace: headers["x-anythingllm-workspace-slug"] ?? meta?.workspace?.slug,
};
}
const ADMIN_TOOLS = ["delete_customer"];
server.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
const { role } = callerOf(request, extra);
const tools = ALL_TOOLS.filter(
(tool) => role === "admin" || !ADMIN_TOOLS.includes(tool.name)
);
return { tools };
});
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
const { role, workspace } = callerOf(request, extra);
if (ADMIN_TOOLS.includes(request.params.name) && role !== "admin") {
return {
isError: true,
content: [{ type: "text", text: "You are not allowed to use this tool." }],
};
}
// Use `workspace` to pick the right tenant database, etc.
return runTool(request.params.name, request.params.arguments, { workspace });
});Things to know
- MCP servers are shared across all workspaces. Every workspace sees the same MCP servers. Use the forwarded workspace to decide what each one is allowed to do or see.
- Usernames and roles can change. If you store permissions on your side, key them on the user ID, which never changes. Use the username for display or for syncing with your own user list.
- The user is looked up once per agent session, and only when
user.usernameoruser.roleis forwarded. A username or role change applies from the next agent session. - Desktop has no users. Only the
workspacefields have any effect on AnythingLLM Desktop.