mctxdocs
Building mcp servers

Channel Events

Push real-time notifications into your subscribers' AI sessions using ctx.emit(). Apps can surface events proactively -- no tool call required.

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

Normally, your App responds to requests: a subscriber asks the AI to do something, the AI calls your tool, your tool returns a result. Channel events flip that model.

With ctx.emit(), your App can push a notification directly into a subscriber's active AI session at any time -- from inside a tool handler, from a background job, from anywhere in your server code. The subscriber sees the event appear in Claude Code without asking for it.

Use channel events to surface things that matter: a build finishing, a deploy failing, a new comment on an issue, a metric crossing a threshold.


How it works

Your App calls ctx.emit() with a message. mctx queues the event and the subscriber's thin client polls for new events. When one arrives, Claude Code displays the notification in the session.

You do not need to understand the queuing or polling mechanics. From your App's perspective, ctx.emit() is a one-line fire-and-forget call.


ctx.emit()

ctx.emit() is available on the same ctx object that carries ctx.userId. You already receive ctx as the third parameter in every tool, resource, and prompt handler.

Basic usage

const deployApp = async ({ environment }, ask, ctx) => {
  await triggerDeploy(environment);
  ctx.emit("Deployment started");
  return `Deploying to ${environment}`;
};
deployApp.description = "Deploy the app to an environment";
deployApp.input = {
  environment: T.string({ required: true, description: "Target environment" }),
};

app.tool("deploy_app", deployApp);

ctx.emit() does not block the tool response. The deploy continues and your tool returns normally regardless of whether the event was queued successfully.

With an event type

Pass an eventType to categorize the event. Subscribers see the type alongside the message:

ctx.emit("PR #42 merged", { eventType: "pull_request" });

With metadata

Attach structured data to an event using meta. Metadata is available to the subscriber's AI client for richer display and filtering:

ctx.emit("Deploy failed on staging", {
  eventType: "deploy",
  meta: {
    environment: "staging",
    status: "failed",
  },
});

Inside a tool handler

Here is a complete example of a tool that emits events at multiple points during a long-running operation:

const runTests = async ({ suite }, ask, ctx) => {
  ctx.emit(`Test suite "${suite}" started`, { eventType: "ci" });

  const results = await executeTestSuite(suite);

  if (results.passed) {
    ctx.emit(`Build #${results.buildId} passed -- ${results.count} tests`, {
      eventType: "ci",
      meta: {
        build_id: String(results.buildId),
        test_count: String(results.count),
        status: "passed",
      },
    });
  } else {
    ctx.emit(`Build #${results.buildId} failed -- ${results.failures} failures`, {
      eventType: "ci",
      meta: {
        build_id: String(results.buildId),
        failure_count: String(results.failures),
        status: "failed",
      },
    });
  }

  return results;
};
runTests.description = "Run a test suite and report results";
runTests.input = {
  suite: T.string({ required: true, description: "Name of the test suite to run" }),
};

app.tool("run_tests", runTests);

Meta key constraints

ConstraintLimit
Key format[a-zA-Z0-9_]+ (alphanumeric and underscores only)
Maximum keys per event10
display_text max length500 characters
event_type max length100 characters
Total event payload4 KB

Hyphens in meta keys are silently dropped by Claude Code. If you use build-id as a key, Claude Code may receive buildid or drop it entirely. Use underscores: build_id.


Writing good event text

Events appear directly in a subscriber's AI session. Keep display text concise, specific, and actionable.

Good examples:

  • Build #847 passed
  • New comment on issue #12
  • Alert: CPU usage above 90%
  • Deploy to production complete
  • Payment webhook received

Avoid:

  • Long paragraphs or multi-sentence descriptions
  • HTML or markdown formatting
  • Instruction patterns (You should now..., Please check..., The AI should...) -- the platform filters these as a secondary defense, but responsibility for sanitizing event text belongs to your App
  • User-generated content passed through without sanitization

If a subscriber's users can influence what ends up in ctx.emit() -- for example, through a comment body or a commit message -- sanitize or truncate that content before passing it to ctx.emit().


Works everywhere. Enhanced in Claude Code.

Channel events are an enhancement, not a requirement.

Your App's tools, resources, and prompts work with every MCP-compatible client -- Claude Desktop, Cursor, any other client that implements the protocol. When subscribers use those clients, your App works exactly as they expect.

When a subscriber uses Claude Code with the mctx thin client installed, they also get real-time event notifications. The thin client polls for events in the background and surfaces them inside the session.

Design your App so it works fully without channel events. Emit events to add value on top -- not as a substitute for returning useful results from your tools.


Rate limits

LimitValue
Events per minute60 per App
Events per hour1,000 per App
Maximum live events500 (unexpired, per App)

Calls to ctx.emit() that exceed these limits are silently dropped. Your tool handler continues normally.

If ctx.emit() cannot reach the events service for any reason, it silently no-ops. Your tool is never blocked by an unavailable events service.


Next steps


See something wrong? Report it or suggest an improvement