mctxdocs
Building mcp servers

User Context (ctx.userId)

How to identify and personalize responses for each subscriber in your App using the stable ctx.userId identifier mctx provides to every handler.

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

Every tool, resource, and prompt handler on mctx receives a third parameter: ctx. The ctx.userId property gives you a stable identifier for the authenticated subscriber making the request.

ctx.userId is typed as string | undefined. In practice it is present for all authenticated requests — the mctx dispatch layer validates authentication before your server is called. The optional typing exists as a defensive safety measure: guard your access so your code compiles correctly under TypeScript strict mode and handles edge cases safely.

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 the current user's preferences";
getPreferences.input = {};

app.tool("get_preferences", getPreferences);

You do not need to do anything special to receive ctx — it is always passed as the third argument when a subscriber calls your App.

What ctx.userId is

ctx.userId is typed as string | undefined. It is stable, opaque, and per-server isolated.

Stable means the same subscriber always gets the same ctx.userId on your server, regardless of which device they use, which session they are in, or when they connect. A subscriber who used your server six months ago has the same ctx.userId today.

Per-server isolated means the same subscriber gets a different ctx.userId on each mctx App they use. Your App cannot correlate users with other Apps, and other Apps cannot correlate users with yours. This is a privacy guarantee: subscribers' activity on one App stays isolated from every other App on the platform.

Opaque means you cannot reverse it to obtain any personal information about the subscriber. You can use it as a key to store and retrieve data, but it tells you nothing else.

Optional typing means the TypeScript type is string | undefined. The value is present for all authenticated requests — the dispatch layer validates authentication before your handler is called. Guard your access with an if check or nullish coalescing so your code compiles under strict mode and handles any unexpected edge cases.

Minimum required versions

ctx.userId is available starting with:

  • @mctx-ai/app >= 1.2.0
  • @mctx-ai/dev >= 1.1.0

Practical example: per-user data storage

The most common use of ctx.userId is as a key for per-user data — preferences, history, state, or anything else you want to scope to a specific subscriber.

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

const app = createServer();

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

const setPreference = async ({ key, value }, ask, ctx) => {
  if (!ctx.userId) return "Unable to save preference: no user context.";
  const existing = userPrefs.get(ctx.userId) ?? {};
  userPrefs.set(ctx.userId, { ...existing, [key]: value });
  return `Saved ${key} for your account.`;
};
setPreference.description = "Save a preference for the current user";
setPreference.input = {
  key: T.string({ required: true, description: "Preference key" }),
  value: T.string({ required: true, description: "Preference value" }),
};

const getPreference = ({ key }, ask, ctx) => {
  if (!ctx.userId) return null;
  const prefs = userPrefs.get(ctx.userId) ?? {};
  return prefs[key] ?? null;
};
getPreference.description = "Retrieve a saved preference for the current user";
getPreference.input = {
  key: T.string({ required: true, description: "Preference key" }),
};

app.tool("set_preference", setPreference);
app.tool("get_preference", getPreference);

export default { fetch: app.fetch };

Two different subscribers calling get_preference with the same key get their own values back — their ctx.userId values are different, so their data is stored under separate keys.

Using ctx.userId in resources and prompts

ctx is also the third parameter for resource and prompt handlers.

// Resource: return the current user's profile
const myProfile = async (params, ask, ctx) => {
  if (!ctx.userId) return JSON.stringify({});
  const profile = await db.get(`profile:${ctx.userId}`);
  return JSON.stringify(profile ?? { userId: ctx.userId });
};
myProfile.mimeType = "application/json";
app.resource("user://me/profile", myProfile);

// Prompt: personalize a welcome message
const welcomePrompt = async (args, ask, ctx) => {
  if (!ctx.userId) return "Welcome! How can I help you today?";
  const name = await db.get(`name:${ctx.userId}`);
  return `Welcome back${name ? `, ${name}` : ""}! How can I help you today?`;
};
welcomePrompt.description = "Personalized welcome message for the current user";
app.prompt("welcome", welcomePrompt);

Where ctx.userId comes from

When a subscriber connects to your App, mctx validates their identity. The dispatch layer derives ctx.userId from the verified subscriber identity and passes it to your handler on every request.

The value is derived, not the raw identity credential itself. This means:

  • The same subscriber produces the same ctx.userId on your App every time, across all devices and sessions
  • The same subscriber produces a different ctx.userId on each App — your App and other Apps share no common identifier for the same subscriber
  • You cannot reconstruct the subscriber's underlying identity from it

What ctx does not contain

ctx.userId is the only field on ctx that carries subscriber information. It does not include email addresses, names, subscription details, or any other personal data. If you need to associate additional information with a subscriber, store it yourself using ctx.userId as the key.

Next steps


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