AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
MCP verified MIT Self-run

Mcp Signal

mcp-roee-tsur-mcp-signal · by Roee-Tsur

Tiny, zero-dependency telemetry SDK for MCP widgets — capture usage in Claude MCP Apps / ChatGPT Apps SDK / mcp-ui and forward it anywhere, even past the host CSP.

No reviews yet
0 installs
25 views
0.0% view→install

Install

$ agentstack add mcp-roee-tsur-mcp-signal

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/mcp-roee-tsur-mcp-signal)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
27d ago

Declared compatibility

Claude CodeClaude DesktopCursorWindsurf

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Mcp Signal? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

mcp-signal

Telemetry for MCP widgets. Capture how your interactive widget is used inside a sandboxed Claude / ChatGPT / mcp-ui host — and forward it anywhere, even past the host CSP.

[](https://github.com/Roee-Tsur/mcp-signal/actions/workflows/ci.yml) [](https://www.npmjs.com/package/mcp-signal) [](https://www.npmjs.com/package/mcp-signal) [](./package.json) [](./dist/index.d.ts) [](./LICENSE)

[Quickstart](#quickstart) · [How the bridge works](#how-the-bridge-works) · [Adapters](#adapters) · [Configuration](#configuration) · [Docs](./docs) · [Demo](#try-the-demo)


Drop mcp-signal into an interactive MCP widget and get the boring-but-essential answers: does it load? which controls get used? where do people drop off? what errors happen inside that sandboxed iframe? Regular web-analytics snippets don't work cleanly in these sandboxes, and there's no standard instrumentation story — this is that layer.

import { createSignal, consoleAdapter } from 'mcp-signal';

const signal = createSignal({
  widgetName: 'weather',
  adapters: [consoleAdapter()],
});

signal.track('forecast_expanded', { day: 'tue' });

That's a working first event. Swap consoleAdapter() for a real destination when you're ready.

Features

  • 🪶 Tiny & dependency-free — ~5.3 kB min+gzip, zero runtime deps. Ships ESM, CJS, a standalone

`` IIFE, and full TypeScript types.

  • 🧱 Escapes the widget sandbox — the recommended bridge routes events through a

model-invisible MCP tool call, so they leave a CSP-locked widget with no CSP changes and your analytics key stays server-side.

  • 🔌 Pluggable adapters — PostHog, webhook, and console included; the core knows no vendor, and

writing your own is a small, documented contract.

  • 🤖 Auto-captures the essentials — load / visible / hidden / close lifecycle, uncaught errors, and

opt-in [data-mcp-signal] clicks — with zero code — plus track() for everything else.

  • 🎛️ Production-minded — size + interval batching, exponential-backoff retries, idempotency keys,

and a beforeSend hook to redact or drop.

  • 🔒 Privacy-first — no phone-home, no fingerprinting, no user identity. You decide exactly what's

sent.

Install

npm install mcp-signal

Usable from TypeScript or plain JS. For a raw-HTML (srcdoc) widget with no bundler, you can also inline the standalone build — see [docs/setup.md](./docs/setup.md). If your MCP server renders the widget HTML, mcp-signal/inline does the inlining for you in one call (injectSignal(html, { bridge: { toolName } })) — no vendoring, no bundler config.

Quickstart

1. See your first events (console — 30 seconds)

import { createSignal, consoleAdapter } from 'mcp-signal';

const signal = createSignal({
  widgetName: 'weather',
  widgetVersion: '1.0.0',
  adapters: [consoleAdapter()],
});

// lifecycle + errors are captured automatically. Add your own:
document.querySelector('#expand').addEventListener('click', () => {
  signal.track('forecast_expanded', { day: 'tue' });
});

Open the console — you'll see mcp_signal_loaded and every track() call.

2. Ship to a real destination — the bridge (recommended)

The reliable production path routes events through a small app-only tool on your MCP server, which forwards them to PostHog (or anywhere). This works even under the strictest host CSP and keeps your analytics key server-side.

In your widget:

import { createSignal, bridgeAdapter } from 'mcp-signal';

const signal = createSignal({
  widgetName: 'weather',
  adapters: [bridgeAdapter({ toolName: 'record_signal' })],
});

On your MCP server (register one tool — the package hands you the descriptor):

import { createSignalReceiver, posthogAdapter, signalToolDefinition } from 'mcp-signal/server';

const receiver = createSignalReceiver({
  adapters: [posthogAdapter({ apiKey: process.env.POSTHOG_KEY, host: 'eu' })],
});

const tool = signalToolDefinition(); // app-only, model-invisible, read-only
server.registerTool(tool.name, tool, (args) => receiver.handleToolCall(args));

signalToolDefinition() sets _meta.ui.visibility: ["app"] (the model never sees the tool — zero context cost) and readOnlyHint: true (silent on ChatGPT, first-use-then-remembered on Claude). Full walkthrough incl. @modelcontextprotocol/ext-apps: [docs/setup.md](./docs/setup.md) · [docs/bridge.md](./docs/bridge.md).

3. Or send direct from the widget (you control the CSP)

import { createSignal, posthogAdapter } from 'mcp-signal';

const signal = createSignal({
  adapters: [posthogAdapter({ apiKey: 'phc_public_key', host: 'eu' })],
});

Direct HTTP only leaves the widget if you allowlist the destination in your resource's CSP. The package generates it for you:

import { cspMeta } from 'mcp-signal';
cspMeta([posthogAdapter({ apiKey: 'phc_x', host: 'eu' })]);
// => { ui: { csp: { connectDomains: ['https://eu.i.posthog.com'] } } }

See the honest trade-offs in [Limitations](#limitations).

How the bridge works

Widgets can't make arbitrary network calls — a strict host CSP blocks them. So instead of fighting the sandbox, the bridge hands each batch to a model-invisible, app-only MCP tool that your server already trusts. Your server forwards it on. No CSP changes, no exposed keys, no context cost.

flowchart LR
  W["widgetbridgeAdapter"] -- "callTool('record_signal', …)" --> H[host]
  H -- "tools/call" --> S["your MCP servercreateSignalReceiver"]
  S -- "server-side POST" --> D[(PostHog / webhook)]

What gets captured

| Event | When | | ------------------------ | ----------------------------------------------- | | mcp_signal_loaded | SDK initializes | | mcp_signal_visible | widget becomes visible | | mcp_signal_hidden | widget is hidden (also flushes) | | mcp_signal_closed | widget is torn down (pagehide) | | mcp_signal_error | uncaught error / unhandled rejection | | mcp_signal_interaction | click on a [data-mcp-signal] element (opt-in) | | (your name) | every track(name, props) call |

Every event carries a best-effort context: widget name/version, an anonymous per-load sessionId, detected host, theme, locale, display mode, timezone, and viewport — only where reliably obtainable, and never any user identity. See [Privacy & data](#privacy--data).

Adapters

| Adapter | Package | Purpose | | ------------------------------------ | ------------------- | ---------------------------------------- | | consoleAdapter() | mcp-signal | Local dev; always works under any CSP | | webhookAdapter({ url }) | mcp-signal | POST batches to any URL | | posthogAdapter({ apiKey, host }) | mcp-signal | PostHog Cloud (US/EU) or self-hosted | | bridgeAdapter({ toolName }) | mcp-signal | Route via an MCP tool call (recommended) | | createSignalReceiver({ adapters }) | mcp-signal/server | Server-side counterpart to the bridge |

Full config for each is in [docs/adapters.md](./docs/adapters.md). Writing your own is a small, documented contract — see [docs/writing-an-adapter.md](./docs/writing-an-adapter.md).

Configuration

createSignal(config) takes these options (all optional). The client it returns exposes track(), flush(), shutdown(), setContext(), getContext(), queueLength, and enabled.

All createSignal options

| Option | Default | Description | | ------------------------------ | -------------------- | ------------------------------------------------------- | | adapters | [consoleAdapter()] | Destinations. | | widgetName / widgetVersion | — | Attached to every event's context. | | enabled | true | false = hard no-op, no listeners attached. | | autoCaptureLifecycle | true | Emit loaded/visible/hidden/closed. | | autoCaptureErrors | true | Capture uncaught errors + rejections. | | autoCaptureInteractions | false | true or { attribute, captureAllClicks, eventName }. | | batchSize | 20 | Flush when the queue reaches this. | | flushIntervalMs | 5000 | Periodic flush; 0 disables. | | maxQueueSize | 500 | Drop oldest beyond this (backpressure). | | requestTimeoutMs | 8000 | Per in-session send timeout. | | retry | {maxRetries:3,…} | Exponential backoff for in-session sends. | | beforeSend | — | (event) => event \| null — redact or drop. | | context | — | Static properties merged into every event's context. | | sessionId | auto | Override the session id. | | host | auto | Override host detection. | | debug | false | Verbose logs + CSP diagnostics. |

Try the demo

git clone https://github.com/Roee-Tsur/mcp-signal
cd mcp-signal
npm install
npm run example        # builds, then serves http://localhost:8787

Click around and watch events arrive through both transports (webhook + bridge) live, in the page and in your terminal. See [example/README.md](./example/README.md).

Privacy & data

The SDK collects nothing on its own and never phones home. It sends exactly what you configure. It attaches non-invasive context (theme, locale, viewport, an anonymous per-load session id — no user identity, no fingerprinting), which you can trim or redact with beforeSend, or disable entirely with enabled: false. You are responsible for your end users' privacy and applicable law. Read the full [Privacy & data](./docs/privacy.md) guide before shipping.

Limitations

mcp-signal is honest about its v0.1 edges:

  • Direct HTTP needs a CSP allowlist. Widgets are network-restricted by the host; the SDK can't

self-authorize egress. Use the bridge, or add the domain via cspMeta().

  • Tool-call approval is a host's call. Read-only tools are silent on ChatGPT and

first-use-then-remembered on Claude, but the spec lets a host prompt; we can't guarantee zero prompts everywhere.

  • Fire-and-forget. Cross-origin sends can't read responses (that's how they stay CSP-simple), so

delivery is best-effort with retries + idempotency keys, not confirmed.

  • Host detection is best-effort. chatgpt vs a framed host isn't always distinguishable

synchronously; pass host to override.

Details and workarounds: [docs/limitations.md](./docs/limitations.md).

Roadmap

  • A hosted dashboard (data lives in your destination for now).
  • More adapters: Segment, Amplitude, GA4, Mixpanel, OpenTelemetry.
  • Drop-in tool-registration helpers for popular MCP server frameworks.
  • Consent / CMP tooling and PII-scrubbing helpers.
  • A durable, cross-load retry queue.
  • Richer opt-in auto-capture.

Not in v0.1: a storage/query backend, a dashboard, and server-side MCP telemetry (tool-call/resource metrics) — widgets only for now.

Contributing

Issues and PRs welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md). The bar for a new adapter: implement the small contract, stay dependency-free, and pass the contract test.

License

[MIT](./LICENSE) © 2026 Roee Tsur

Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.