mctxdocs
Build Your MCP Server

User Identity

How ctx.userId works — what it is, why it's isolated per server, when it's undefined, and how to use it safely.

Need help? Connect help.mctx.ai for instant answers.

What ctx.userId is

ctx.userId is a stable, verified identifier that mctx injects into every handler call after the subscriber authenticates. It is not an email address, not a session token, and not something the client passes in — it is a platform-provided fact about who is making the request.

const getPreferences = async (args, ask, ctx) => {
  if (!ctx.userId) return { theme: "light", language: "en" };
  const prefs = await db.get(`prefs:${ctx.userId}`);
  return prefs ?? { theme: "light", language: "en" };
};
getPreferences.description = "Fetch preferences for the current user";
getPreferences.input = {};

server.tool("get_preferences", getPreferences);

The value is stable across sessions. A subscriber who used your MCP server six months ago has the same ctx.userId today, regardless of which device or client they connected from.

Why per-server isolation matters

The same subscriber has a different ctx.userId on each mctx MCP server. Your MCP server and every other MCP server on the platform share no common identifier for the same subscriber.

This means you cannot correlate users across servers, and other MCP servers cannot correlate their users with yours. It is a privacy guarantee built into the platform: subscriber activity on one MCP server is isolated from every other MCP server.

Use ctx.userId freely as a storage key within your own MCP server. Do not expect it to match any value from another mctx MCP server.

When it's undefined

ctx.userId is typed as string | undefined. In practice, all requests that reach your handler through the mctx platform carry an authenticated user identity. The undefined case exists for requests that arrive outside the normal authenticated path — for example, when running locally with mctx-dev, or in HTTP transport without a connected mctx session.

Always guard access with a null check or nullish coalescing so your code compiles under TypeScript strict mode and degrades gracefully:

const userId = ctx?.userId;
if (!userId) return "No user context available.";

Example: per-user data storage

import { createServer, T } from "@mctx-ai/mcp";

const server = createServer();

// In-memory store for this example.
// In production, use a database, KV store, or external API keyed by userId.
const store = new Map();

const saveNote = async ({ content }, ask, ctx) => {
  if (!ctx?.userId) return "Cannot save: no user context.";
  store.set(ctx.userId, content);
  return "Note saved.";
};
saveNote.description = "Save a note for the current user";
saveNote.input = {
  content: T.string({ required: true, description: "Note content" }),
};

const getNote = (args, ask, ctx) => {
  if (!ctx?.userId) return null;
  return store.get(ctx.userId) ?? null;
};
getNote.description = "Retrieve the saved note for the current user";
getNote.input = {};

server.tool("save_note", saveNote);
server.tool("get_note", getNote);

export default { fetch: server.fetch };

Two subscribers calling get_note each receive their own content — their ctx.userId values are different, so their data lives under separate keys.

One gotcha

Do not include ctx.userId in logs you ship to an external log aggregator or analytics service. Treat it as per-server PII even though the value is opaque — it is a stable identifier that can be used to track a subscriber's behavior on your MCP server over time. Log it locally for debugging, but scrub it before forwarding logs off-platform.

Next steps


See something wrong? Report it or suggest an improvement — your feedback helps make these docs better.