MCP tools over a live qyl collector — traces, logs, metrics, sessions, and a graph view of agent runs you can watch while they execute.
The repository ships three things:
| What it is | How you get it | |
|---|---|---|
| server | An MCP server exposing qyl telemetry as tools | Hosted at mcp.qyl.at, or npm qyl-mcp-server |
| workbench | A local MCP client for inspecting other people's servers | Run from a checkout on 127.0.0.1:18888 |
| dashboard | The HTTP UI the server serves | Bundled into the server |
The server is a closed world — a fixed, generated tool surface projecting the collector's data. The workbench is an open world — it talks to servers it did not write and validates their schemas at runtime. They are separate deployables because a browser cannot be an MCP stdio client, which is the same split the MCP Inspector makes.
Bun 1.4 is the runtime and the only package manager: bun install, bun run build, bun run test, one bun.lock. The HTTP entry is a web-standard fetch
handler served by its default export, so serving it needs Bun; the published
--stdio binary is the one thing that also runs under plain Node 24, because
that is how npx clients launch it. Architecture lives in the
qyl README, which states the shape and
names the gates that enforce it; this file does not restate it.
Nothing to install. Point an MCP client at:
https://mcp.qyl.at/mcp
It is an OAuth 2.1 resource server, so an unauthenticated request answers 401
with an RFC 9728
protected-resource document. The document identifies the Auth0 issuer and
qyl:read scope. Authentication also requires the client to be registered and
granted access in Auth0; discovery alone does not establish a login.
https://mcp.qyl.at/ is a product page and /healthz is the platform
healthcheck. Neither is a protocol endpoint — /mcp is the only one.
Use https://mcp.qyl.at/mcp in every client. These are the settings to use
after the Auth0 client and qyl:read grant are provisioned. Live OAuth and
tool-call checks remain pending until that configuration and the server changes
are deployed.
| Client | Connection setting | OAuth client path |
|---|---|---|
| ChatGPT web | Enable Developer mode in Settings → Security and login; in ChatGPT Plugins add an MCP connection to https://mcp.qyl.at/mcp, select OAuth, and include qyl:read in Base scopes. |
CIMD at https://chatgpt.com/oauth/client.json, with redirect https://chatgpt.com/connector_platform_oauth_redirect, when the connection page shows stable callbacks. The document supports none and private_key_jwt (currently preferred). DCR is also supported when explicitly selected in the connection form. |
| claude.ai | Customize → Connectors → Add custom connector; enter https://mcp.qyl.at/mcp and leave optional client ID and secret empty. |
CIMD at https://claude.ai/oauth/mcp-oauth-client-metadata, a public client (none) with redirect https://claude.ai/api/mcp/auth_callback; DCR is the fallback. |
| Claude Code | claude mcp add --transport http qyl https://mcp.qyl.at/mcp; then open /mcp in Claude Code to authorize. |
CIMD at https://claude.ai/oauth/claude-code-client-metadata, a public client (none) with port-independent http://localhost/callback and http://127.0.0.1/callback redirects; DCR is the fallback. |
| Codex CLI | codex mcp add qyl --url https://mcp.qyl.at/mcp --oauth-resource https://mcp.qyl.at/mcp; codex mcp login qyl --enable mcp_2026_07_28 --scopes qyl:read; start Codex with codex --enable mcp_2026_07_28. |
CIMD or DCR as selected by the client and Auth0; --oauth-client-registration cimd or dcr can pin a test path. The installed CLI requests 2025-06-18 without that feature, and this server rejects it. |
| MCP Inspector | Select Streamable HTTP, enter https://mcp.qyl.at/mcp, set protocol era to modern, and use Open Auth Settings → Quick OAuth Flow. For CLI checks use npx @modelcontextprotocol/inspector@2.8.0 --cli https://mcp.qyl.at/mcp --transport http --protocol-era modern --method tools/list. |
CIMD when supported by the Inspector release, otherwise DCR. |
Local QYL_DEMO=1 v2 checks on 30 September 2026: Claude Code 2.1.285,
Codex CLI 0.158.0-alpha.2.1 with --enable mcp_2026_07_28, and MCP Inspector
2.8.0 each called list_metrics and received 3 instruments with
has_more: false. Inspector listed all 11 tools, each with qyl:read in
_meta.securitySchemes and readOnlyHint: true. These local calls did not use
OAuth. ChatGPT web, claude.ai, and each client's production OAuth path still
need a deployed build and Auth0 client grants before their results can be
recorded here.
This server accepts only MCP revision 2026-07-28. A client that opens with the
2025-era initialize method receives -32022; there is no v1 mode.
npx qyl-mcp-server --stdioWithout --stdio it serves Streamable HTTP (revision 2026-07-28 only) on
http://127.0.0.1:3001/mcp; set PORT to change it. The local default binds to
loopback only and accepts local or absent browser origins.
Point it at a collector:
export QYL_COLLECTOR_URL=http://127.0.0.1:5100
export QYL_API_KEY='your-collector-key' # omit for an unsecured local collector
npx qyl-mcp-server --stdioQYL_API_KEY is an outgoing collector credential. It does not authenticate
incoming MCP clients — the local server has no inbound auth, which is why it
binds to loopback.
The authoritative surface is
server/tool-manifest.snapshot.json,
generated from the contract and checked in. Tools marked
meta.ui.visibility: ["app"] are called by the bundled MCP Apps, not by a model.
Traces, logs, and sessions have readers and an explorer; CI runs have ci_log;
metrics have list_metrics, get_metric_series, and query_metric.
Telemetry: list_traces, get_trace, list_sessions, search_logs, ci_log,
list_metrics, get_metric_series, query_metric, display_traces,
display_mcp_dashboard. Every one is read-only.
In ChatGPT the two apps are also plugin extension
entry points: Trace Explorer (display_traces) opens from the sidebar and
as a thread tab, and MCP Dashboard (display_mcp_dashboard) from the
sidebar. Both open with {} (recent traces, the last 24 hours) and render in
inline or fullscreen. Other clients ignore the openai/ui metadata.
A hosted server with MCP_EVENTS_STORE set advertises events in
server/discover and answers events/list, events/subscribe and
events/unsubscribe behind the same OAuth gate as the tools, following
ChatGPT's MCP Events
and the draft
design sketch.
- One event,
trace.error: a trace with at least one error span reached the collector. The optionalservice_nameargument narrows it to one service. The payload carriestrace_id,root_span,services,span_count,duration_msandstart_time, so the model can follow up withget_traceordisplay_traces. - Webhook delivery only. The callback must be
httpsto a public address; it is resolved and checked on every connection, and redirects are not followed. Before storing a subscription the server sends a signed, single-use challenge and requires it echoed; a failure is-32015with adata.reason. - Each event is one POST signed with
Standard Webhooks
(
webhook-id=eventId=evt_<trace_id>, plusX-MCP-Subscription-Id), retried with backoff on network errors,429and5xx;410ends the subscription. - Subscriptions are keyed by caller, callback URL, event and arguments, last one
hour by default and one day at most, and survive restarts in the store file.
A caller holds at most 20 live subscriptions; a refresh always passes.
Delivery starts from the first collector poll after subscribing (every 30 s,
MCP_EVENTS_POLL_MSto change it) and carriescursor: null: there is no replay, so traces that arrive while the server is down are not delivered.
A local client for connecting to MCP servers you did not write, inspecting their negotiated surface, invoking tools safely, and keeping the evidence.
bun install --frozen-lockfile
bun run build
bun run start:workbenchOpen http://127.0.0.1:18888. Set QYL_MCP_WORKBENCH_PORT for another port.
The dashboard bootstraps an opaque HttpOnly, SameSite=Strict loopback
session. Tokens are hashed in memory, never returned in API payloads, and do not
survive a restart. Host and browser-origin checks protect the loopback API from
DNS rebinding and cross-origin requests.
Connecting a server. Choose Add server. Streamable HTTP takes a
credential-free endpoint plus header references like
Authorization=MCP_TOKEN|bearer. stdio takes a command, one argument per line,
an optional working directory, and environment mappings like
SERVER_TOKEN=MCP_SERVER_TOKEN. Only variable names are sent by the browser or
persisted; values resolve in the runner at connection time and register with the
shared redactor. Endpoints cannot embed credentials, query values, or fragments,
and persistent Cookie headers are rejected.
Starting a stdio server launches code with your permissions, so review the exact executable, arguments, working directory, and environment references first.
Protocol. The server speaks revision 2026-07-28 only. A 2025-era host is
refused with -32022 naming that revision; a host that opens with
server/discover connects. There is no fallback and no setting.
Safety. Tool annotations are hints, not permissions. Only a tool explicitly marked read-only, non-destructive, and closed-world runs without confirmation. Missing, contradictory, mutating, destructive, or open-world hints require you to approve the exact call. The runner never synthesizes a confirmation. Arguments, results, protocol payloads, persisted evidence, diagnostics, and telemetry all pass through credential and URI redaction.
What persists. State defaults to ~/.qyl/mcp-workbench.json
(QYL_MCP_STATE_PATH overrides). Workspaces, server definitions, executions,
protocol evidence, tests, suites, evaluation runs, and exports are written by
atomic replacement with mode 0600; a directory the app creates gets 0700,
while an existing parent you supply is left alone. Work interrupted by a restart
is restored as explicit failure evidence, not silently dropped.
The invocation composer keeps a generated form and a raw JSON view in sync, validates input in a deadline-bounded worker, applies an execution timeout, and sends an idempotency key. JSON Schema and pattern assertions run on that same isolated path — the browser never compiles a server-supplied regular expression.
Each execution retains its request, result or typed error, lifecycle, duration, attempts, cancellation state, redacted JSON-RPC timeline, and trace correlation. Live protocol and execution streams use resumable event identifiers, and cancelling aborts the in-flight SDK request. The tests workspace persists real invocations with status, exact, partial, JSON Schema, pattern, and latency assertions; suites run with bounded concurrency and export as contract-validated JSON or Markdown with SHA-256 artifact evidence.
The server also records inbound tools/call requests natively — including over
plain stdio, with no workbench involved — to
~/.qyl/mcp-native-executions.json (QYL_MCP_NATIVE_STATE_PATH overrides),
newest 1,000 retained. Results under two million serialized characters are kept
in full after redaction; larger ones are replaced by an explicit truncation
result rather than silently trimmed. Token usage and cost are kept only when a tool reports
explicit structured evidence; qyl.mcp never infers them from prose, latency, or
payload size.
| Variable | Purpose |
|---|---|
QYL_COLLECTOR_URL |
Collector read API base; default http://127.0.0.1:5100. Also the OTLP base when set. |
QYL_API_KEY |
Collector read and OTLP credential. Outgoing only. |
QYL_PROJECT |
Server-owned collector project scope; defaults to default. |
QYL_OTLP_ENDPOINT |
Optional OTLP base for workbench self-telemetry. |
QYL_MCP_TELEMETRY=0 |
Disable MCP spans, metrics, and operation logs. Enabled otherwise. |
QYL_MCP_CAPTURE_CONTENT=1 |
Include redacted, size-bounded request and response bodies in operation logs. Off by default. |
QYL_MCP_STATE_PATH |
Override the durable workbench JSON path. |
QYL_MCP_NATIVE_STATE_PATH |
Override the native-execution evidence path. |
QYL_MCP_WORKBENCH_PORT |
Workbench listener port; default 18888. |
QYL_DEMO=1 |
Explicit offline demo mode. |
Demo mode is deliberate and labelled — results carry mode: "demo". A collector
error stays an error; live mode never silently falls back to generated data.
qyl.mcp exports correlated MCP spans, duration histograms, and metadata-only operation logs over OTLP, targeting OpenTelemetry semantic conventions v1.43.0 and its development MCP conventions.
mcp.client and mcp.server spans are named {mcp.method.name} {target} when a
low-cardinality target exists, and never carry argument or result content.
Failures use error.type on the same operation histogram rather than a second
counter. The qyl.mcp.operation log event carries matching trace context; its
body is metadata-only unless you opt in with QYL_MCP_CAPTURE_CONTENT=1.
Trace context and baggage travel in the unprefixed MCP params._meta bag on
every supported transport. An inbound server span parents off that remote context
and links any ambient transport span. HTTP propagation stays a separate concern
and is never replaced by the MCP carrier.
The built-in qyl-telemetry server reads real traces, logs, metrics, and sessions from
QYL_COLLECTOR_URL. Those reads run under async self-export suppression, so
inspecting qyl evidence does not generate recursive MCP telemetry.
Signal-specific OTEL_EXPORTER_OTLP_{TRACES,METRICS,LOGS}_ENDPOINT take
precedence; otherwise the base order is QYL_OTLP_ENDPOINT,
QYL_COLLECTOR_URL, OTEL_EXPORTER_OTLP_ENDPOINT, http://127.0.0.1:4318.
Railway settings live in .railway/railway.ts (Railway Infrastructure as Code).
The file is not read at deploy time: preview with railway config plan and
push it to your project with railway config apply. Use / as the root
directory:
Build: bun run --cwd server build
Start: bun server/dist/main.js
Health: /healthz
The start command is Bun, not Node: the HTTP entry exports a fetch handler and
refuses to serve under Node. Both commands and the /healthz check are declared in
.railway/railway.ts and applied to Railway with railway config apply; nothing is read
from the repository at deploy time.
Do not set PORT; Railway injects it. The MCP endpoint is stateless; the one
piece of state is the MCP Events subscription file named by MCP_EVENTS_STORE,
which .railway/railway.ts keeps on the volume mounted at /data. Leave the
variable unset to run without events and without a volume. Railway's 15-minute
streaming limit applies to unusually long synchronous operations.
To submit the server as a ChatGPT plugin, use the portal's With MCP path
with https://mcp.qyl.at/mcp. When the portal shows its domain-verification
token, set OPENAI_APPS_CHALLENGE to it; the server then answers
/.well-known/openai-apps-challenge with exactly that token as plain text.
NODE_ENV=production \
MCP_BIND_HOST=0.0.0.0 \
MCP_PUBLIC_URL=https://mcp.example.com \
MCP_ALLOWED_HOSTS=mcp.example.com,<service>.up.railway.app,healthcheck.railway.app \
MCP_ALLOWED_ORIGIN_HOSTS=mcp.example.com,<service>.up.railway.app \
QYL_COLLECTOR_URL=http://qyl-collector.railway.internal:8080 \
QYL_API_KEY='<collector-api-key>' \
bun run startMCP_PUBLIC_URL adds its hostname to the Host and Origin allowlists, and
<public-url>/mcp is the fixed resource identifier tokens are audience-bound to.
A non-loopback bind requires it.
The default Auth0 mode accepts only the qyl production Auth0 issuer
https://qyl-eu.eu.auth0.com/, which is pinned in the build and is not
configurable: there is no environment variable that substitutes an arbitrary
OAuth issuer. Configure the API audience for your public URL in that tenant
instead.
The hosted Auth0 profile uses CIMD or DCR + OAuth 2.1 Authorization Code with
PKCE S256 + issuer identification. ChatGPT's current stable CIMD supports
none and private_key_jwt (its published preference); Claude's CIMD is a
public client using none. The server is an OAuth
resource server: Auth0 registers clients, authenticates users and clients,
performs grant exchanges, and issues tokens. qyl.mcp never hosts a token endpoint,
accepts a client assertion as an access token, or forwards a caller's token to
the collector. Its outgoing collector credential remains separate.
A public URL always builds the authentication gate. A non-loopback bind without
MCP_PUBLIC_URL is refused. Startup discovers the pinned Auth0 issuer and verifies
CIMD, DCR, S256, authorization-code, both client authentication methods and
response issuer-identification capabilities. Missing advertised capabilities
stop startup. Discovery does not prove that DCR is enabled, an individual client
has been provisioned, or that an Auth0 plan permits private_key_jwt.
Configure Auth0 as follows:
- Register the API with identifier
<public-url>/mcp, RS256, the RFC 9068 access-token profile, and the permissionqyl:read. - Enable Client ID Metadata Document Registration, the Resource
Parameter Compatibility Profile, and authorization response issuer
identification so every authorization response includes the exact
iss. - Import each CIMD URL through Applications → Create Application → Import
from URL:
https://chatgpt.com/oauth/client.json,https://claude.ai/oauth/mcp-oauth-client-metadata, andhttps://claude.ai/oauth/claude-code-client-metadata. Use the exact ChatGPT client ID and callback shown in its connection page if it selects a callback-specific identity. Preserve each document's redirect URIs and token method. ChatGPT publishesprivate_key_jwtas its preference and a JWKS; Auth0 documents that method as Enterprise-only. Its metadata also supportsnone, so verify the selected method with a real token exchange. Claude usesnone, and Claude Code needs both loopback hostnames with any port. - Enable Dynamic Client Registration in Settings → Advanced and set
dynamic_client_registration_security_modetostrict. Auth0's DCR endpoint is open to registration when enabled. Restrict it with a Tenant ACL where practical, and keep third-party API default permissions empty. A new DCR client then needs its own explicitqyl:readclient grant before login can receive that scope; use a test client ID to verify this path. Do not set a tenant-wide defaultqyl:readgrant. - Grant each intended CIMD or DCR client
qyl:readfor the API and permit only the intended users to log in. Refresh imported CIMD metadata when client keys or callbacks change.
qyl:read permits access to the deployment's traces, logs, metrics, sessions,
and CI evidence. Every current tool is read-only, so no write permission is
advertised. Client registration does not itself authorize API access: grant it
only to intended clients and users. An open default qyl:read grant would
expose the same evidence to anyone who completes authorization.
Every MCP request passes the same verifier: RS256 signature, exact issuer,
resource audience, expiry, not-before, RFC 9068 token type, subject, client ID,
issued-at and token ID. ID tokens, ID-JAGs and client assertions are rejected.
Credentials must use Authorization: Bearer; query tokens and malformed or
multiple credentials receive 400. Missing/invalid/expired tokens receive 401;
insufficient scopes receive 403 with insufficient_scope. Challenges contain
resource_metadata and the complete required scope set. offline_access is
never a resource requirement. The resource publishes RFC 9728 metadata and
mirrors the validated provider's OAuth metadata, preserving its extension
fields. Both /.well-known/oauth-protected-resource/mcp and
/.well-known/oauth-protected-resource describe the same /mcp resource.
Auth0 itself publishes OIDC discovery at
https://qyl-eu.eu.auth0.com/.well-known/openid-configuration. This resource
server does not publish OIDC discovery on mcp.qyl.at: it is not the issuer.
If ChatGPT workspace domain claiming is needed, enable openid and email in
Auth0 and its verified-email UserInfo response; the resource scope remains
qyl:read.
Extensions are off by default and enabled with exact integration versions:
MCP_AUTH_EXTENSIONS=enterprise-managed-authorization@1.0.0,oauth-client-credentials@1.0.0| Integration | Upstream status | Additional provider requirement |
|---|---|---|
enterprise-managed-authorization@1.0.0 |
Stable | JWT bearer grant and authorization_grant_profiles_supported containing urn:ietf:params:oauth:grant-profile:id-jag |
oauth-client-credentials@1.0.0 |
Draft; explicitly opt in | Client credentials grant with private_key_jwt and advertised asymmetric signing algorithms |
The version after @ is qyl.mcp's integration version, independent of the MCP
wire revision 2026-07-28 and the upstream specification's status. There is no
latest alias. Each module records its source specification. Unknown versions,
duplicate IDs, conflicting requirements and unsupported provider capabilities
stop startup. These integrations apply only to hosted Auth0 mode; selecting
them in loopback-only or Cloudflare Access mode is a configuration error.
These are resource-server integrations of the MCP authorization extensions. For enterprise authorization, the client exchanges its enterprise identity for an ID-JAG at its IdP, then exchanges that ID-JAG for an access token at Auth0. Configure the IdP trust, resource policy and client grants in those providers. For client credentials, provision the machine client and its keys at Auth0 and grant only the required API permissions. ChatGPT's interactive connection uses the core authorization-code flow; enabling an extension does not add a client flow that ChatGPT does not support.
Both flows finish with an Auth0 access token for this resource, which passes exactly the same core verifier and scope gate. qyl.mcp does not consume the intermediate identity assertions, issue tokens, or implement an enterprise IdP. Extensions do not change the core MCP envelope, claim extra protocol capabilities, or synthesize provider discovery fields. They compose by uniting provider requirements; disabling every extension preserves core authorization. Selecting a module checks the resource server's provider contract; granting or revoking a client flow remains an Auth0 administrative policy, not a claim the resource server can infer from arbitrary token claims.
Embedders can import qyl-mcp-server/auth and register new declarative
AuthorizationExtension modules through resolveAuthorizationExtensions and
loadHostedOAuth's options (see the example below). Extensions cannot
replace the core verifier or grant scopes. createResourceAuthorization supports
explicit scopeImplications for deployments with scope hierarchies. Implications
are transitive and cycle-safe; wildcard-looking scope names have no implicit
meaning, and verified token claims remain unchanged. The standalone server has
only qyl:read, so it does not invent a broader scope or a hierarchy.
import { authorizationExtensions, resolveAuthorizationExtensions, loadHostedOAuth }
from "qyl-mcp-server/auth";
const extensions = resolveAuthorizationExtensions(
["enterprise-managed-authorization@1.0.0"],
authorizationExtensions,
);
const oauth = await loadHostedOAuth(new URL("https://mcp.example.com/mcp"), { extensions });Client refresh-token storage and step-up retry limits belong to the connecting
MCP client. This server neither stores refresh tokens nor runs authorization
retries. Clients should accumulate previously requested and newly challenged
scopes and bound retries as specified by MCP. The server returns all scopes
required for the current operation together (qyl:read for its current surface).
References: MCP authorization, Auth0 CIMD, ChatGPT authentication.
To switch an existing hosted server to Cloudflare Access, set all three values:
MCP_AUTH_PROVIDER=cloudflare-access
MCP_ACCESS_TEAM_DOMAIN=https://YOUR-TEAM.cloudflareaccess.com
MCP_ACCESS_AUD=YOUR-ACCESS-APPLICATION-AUDMCP_PUBLIC_URL remains required. Configure a self-hosted Access application
for that public hostname in the same Cloudflare account, enable Managed OAuth,
and allow only the intended identities. Use its actual team domain and AUD tag.
Do not turn on this mode until the Access application is configured.
Access performs the client OAuth flow and owns its discovery endpoints. The
server validates Cf-Access-Jwt-Assertion with the team's signing keys, RS256,
issuer, application audience, expiry and subject before serving /mcp. A direct
request to the Railway origin without a valid assertion is rejected. Opaque
oauth: bearer tokens alone are not accepted by the origin. Access membership
grants this server's existing read-only qyl:read tool surface; the assertion
itself does not carry qyl scopes or identify the originating OAuth client.
The default MCP_AUTH_PROVIDER=auth0 retains the pinned Auth0 behavior above.
Unknown modes, partial Access settings and non-Cloudflare team URLs fail closed.
This mode is an available deployment configuration, not a claim that the public
service has already switched providers. Verify the public discovery response
and an authenticated tool call after the actual cutover.
Cloudflare Managed OAuth documentation
bun install --frozen-lockfile
bun run build
bun run test
bun run smoke
bun run smoke:otlp
bun run smoke:livesmoke exercises explicit demo behavior. smoke:otlp needs the sibling qyl
collector checkout (or QYL_COLLECTOR_PROJECT pointing at it), starts an
API-key-protected collector, and drives its real OTLP/protobuf and read surfaces
— a fixture validated by a schema from this repository would prove nothing about
interoperability. smoke:live needs the published qyl dotnet tool on PATH
(dotnet tool install -g qyl): it starts qyl up under a timeout, sends
OTLP/JSON traces, logs and metrics, then calls every one of the eleven tools in
live mode over stdio, reads both MCP App resources, and repeats tools/list
and one call over Streamable HTTP against a second process. It is the only gate
that proves the metrics, session, CI and display tools against real data.
Every tool shape in server is contractSchema("<definition>") from @ancplua/qyl-api-schema/zod: the contract package binds each published definition name to its TypeScript type, so a shape cannot be paired with the wrong type and a hand-rolled z.object( has nothing to stand in for. The textual gate that used to guard this (verify:shapes) is gone with the reason for it.
- Usage and cost appear only when execution evidence records them. qyl.mcp does not estimate.
- Downstream spans from an external or stdio peer correlate only when that peer honors MCP propagation metadata. qyl.mcp cannot retrofit instrumentation into an uninstrumented server.
- The live connection journal is process-local. Execution, test, and evaluation evidence is durable, but protocol traffic not attached to retained evidence is not reconstructed after a restart.
- Conformance coverage is local stdio and Streamable HTTP. External remote services cannot be verified without their endpoints and credentials.
Request, response, event, and error models come from
qyl-api-schema. MCP envelopes come
from the official MCP TypeScript SDK 2.0.0. OTLP payloads come from the official
OpenTelemetry SDK. None of the three is mirrored or hand-built here.