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.
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
- Tools — build actions your MCP server can perform
- Resources — expose data for AI context
- Prompts — create guided conversation templates
- Framework API Reference — every export, type, and option in
@mctx-ai/mcp
See something wrong? Report it or suggest an improvement — your feedback helps make these docs better.
Prompts
Reusable conversation templates the MCP client can hydrate with variables. Learn handler signatures, the conversation() builder, and how to inject user context.
Sampling
Use ask to request LLM completions from the connected client. Learn when ask is null, how to guard it, and how to use SamplingOptions for full control.