Pipelex MCP exposes registered-method discovery, MTHDS validation, inputs
projection and preparation, and durable method runs to MCP hosts, wrapping the Pipelex API through the
@pipelex/sdk PipelexApiClient. It ships as two servers from one repo and
one capability core:
- Hosted console — a Skybridge HTTP server (deployed on Alpic) for remote-connector hosts (ChatGPT, claude.ai, Claude Desktop, Cowork). Registers the Skybridge views.
- Local workshop — an npm-distributed stdio server (
@pipelex/mcp, binpipelex-mcp) that coding-agent hosts (Claude Code, Codex, Cursor, Cowork) spawn vianpx. Its headline feature is the{ path }file arm: it reads.mthdsfiles from disk instead of having the model hand-copy their contents.
Both servers register the same MCP tools, with identical names, schemas, and contracts — with one documented exception, marked below:
| Tool | What it does |
|---|---|
mthds_list_methods |
List the active API key's organization catalog as bounded names, descriptions, and canonical ids — never method source or stored inputs/outputs. |
mthds_validate |
Validate submitted .mthds files, or a registered method by catalog id; on a valid verdict, ship the dry-run method graph to the run-graph view (hosted only). |
mthds_inputs_template |
Project a pipe's declared inputs as a fill-in template for a run. |
mthds_prepare_inputs |
Turn filled inputs run-ready: upload file-bearing values to Pipelex storage and rewrite them to pipelex-storage:// (workshop uploads; console is pass-through only). |
mthds_upload_attachments |
Hosted console only. Turn a file the user attached in the chat into a run-ready pipelex-storage:// reference (ChatGPT only — see Chat attachments). |
mthds_run |
Start a durable run on the hosted Pipelex API; returns a durable run_id immediately. |
mthds_run_status |
Check a durable run's coarse lifecycle state by run_id. |
mthds_run_results |
Fetch a durable run's terminal outcome by run_id. |
mthds_upload_attachments is the exception: its sole argument is a
host-substituted attachment reference, and the host gates that substitution on
the declared JSON Schema, so on the workshop the tool would be structurally
unreachable rather than merely unused. The invariant that still holds is that
no tool name means different things on the two shells.
SPEC.md is the source of truth for the full tool contracts, verdict
discipline, and view behavior. This README covers what you need to install,
register, and run the servers.
MCP tool arguments are generated token-by-token by the host LLM — there is no
other channel from the conversation to the server. So submitting a bundle's
.mthds contents to the hosted server means the model re-emits every file
as output tokens (slow on large bundles, re-paid every repair-loop iteration and
every tool in the chain, and not guaranteed byte-identical to what's on disk).
The local server sidesteps this: the host spawns it in your workspace, so it
can read files from disk given only a path.
The shared submitted-files shape accepts two item forms — inline content or a file path:
type SubmittedFileInput = { content: string; uri?: string | null } | { path: string };Both servers register this same union, so the tool contract never forks; what differs is behavior:
- The workshop resolves
{ path }from disk before invoking the capability — near-constant token cost regardless of bundle size, byte-accurate reads, and real provenance (the resolved item carriesuri= the submitted path, so diagnostics locate to files you can open and edit). Inline{ content, uri? }items stay accepted for parity. - The console rejects
{ path }items with an instructiveinput_domainerror located atfiles[i].path: this deployment cannot read files; resubmit as{ content, uri? }, or use the local workshop (npx @pipelex/mcp).
An item is one arm or the other; on a malformed item carrying both keys,
content wins (first-match union semantics) and path is ignored.
Path trust boundary (workshop). { path } values resolve relative to the
server's working directory. The arm is contracted to .mthds files, so a
non-.mthds extension is rejected before any filesystem access (a
prompt-injected .env or key-file path is never opened), and the resolved
target (symlinks followed) must live inside the working-directory subtree.
Non-.mthds paths, escapes, missing files, and non-regular files come back as
input_domain errors located at files[i].path.
The workshop is published as @pipelex/mcp.
Hosts spawn it on demand with npx -y @pipelex/mcp (bin pipelex-mcp); you do
not install it globally. It needs Node.js 24+ and a PIPELEX_API_KEY (a
plx_sk_ platform key) for the run tools; validation and inputs work without one
against a key-less API.
The registration name is yours to choose; these snippets use pipelex (which
yields mcp__pipelex__mthds_validate-style tool names).
Claude Code
claude mcp add pipelex --env PIPELEX_API_KEY=plx_sk_... -- npx -y @pipelex/mcpCodex — ~/.codex/config.toml
[mcp_servers.pipelex]
command = "npx"
args = ["-y", "@pipelex/mcp"]
env = { PIPELEX_API_KEY = "plx_sk_..." }Cursor — ~/.cursor/mcp.json
{
"mcpServers": {
"pipelex": {
"command": "npx",
"args": ["-y", "@pipelex/mcp"],
"env": { "PIPELEX_API_KEY": "plx_sk_..." }
}
}
}Cowork / Claude Desktop (builder mode) — claude_desktop_config.json
{
"mcpServers": {
"pipelex": {
"command": "npx",
"args": ["-y", "@pipelex/mcp"],
"env": { "PIPELEX_API_KEY": "plx_sk_..." }
}
}
}Environment
PIPELEX_API_KEY— aplx_sk_platform key. Required formthds_list_methodsbecause the returned catalog is the key's active, workspace-shared organization catalog. Optional formthds_validate/mthds_inputs_templatecalls that submitfilesagainst a key-less API; effectively required for the run family and for anymethod_idcall on any tool, since the catalog is org-scoped (a missing/invalid key is aconfigno-verdict).PIPELEX_BASE_URL— defaults to the hosted Pipelex API (https://api.pipelex.com). Set it tohttp://localhost:8081to develop against a local OSSpipelex-apirunner. Durable runs need the hosted API; a bare runner has no run lifecycle.
The hosted console holds no server-side API key and there is nothing to paste. Add the connector by its URL and your host walks you through signing in with your Pipelex account:
https://<console-url>/mcp
Sign-in is OAuth through WorkOS AuthKit, which the console's MCP host drives for you — ChatGPT, claude.ai, Claude Desktop/Cowork and Cursor all handle the handshake themselves, including picking the organization you want to work in. Your verified session is what authorizes every call the console makes on your behalf, so the catalog you see and the runs you spend are your own. The token never travels through tool arguments, so it never enters the model's context.
There is no keyless mode: every tool call requires a signed-in session. If
one expires or is revoked, calls come back as a config no-verdict at
authorization telling you to reconnect the connector and sign in again.
Upgrading from a
?api_key=connector. Bring-your-own-key has been removed. A connector still registered with?api_key=plx_sk_...(or anAuthorization: Bearer plx_sk_...header) no longer connects at all — remove it and re-add it by the plain URL above. ChatGPT in particular caches a connector's configuration at add-time, so re-adding is the only path.
(That said, prefer the local workshop on hosts that can spawn it — see the matrix below.)
The workshop gets the user's actual file through the { path } arm. The console
has no filesystem, so it gets it a different way: ChatGPT's Apps runtime
rewrites the model's reference to an attached file into a signed-URL object
before the call reaches the server. mthds_upload_attachments takes that
channel — it fetches the bytes server-side and uploads them to Pipelex storage
under your signed-in account, returning only small URI strings. The bytes never enter
the model's context, which is the whole reason console-side upload is allowed
here at all.
The flow, on the console:
user attaches a PDF in the chat
→ mthds_upload_attachments → pipelex-storage://… uris
→ fill the uris into the mthds_inputs_template output
→ mthds_run
mthds_prepare_inputs can be skipped — a pipelex-storage:// value is
already run-ready. Nothing else in the flow changes.
Three things to know:
- Re-add the connector to get it. ChatGPT caches a connector's tool list at add-time and never refreshes it, so a newly shipped tool (or a changed tool description) stays invisible to an existing installation until you remove and re-add the connector.
- 7 MiB per attachment. This is a transport ceiling, not a product choice:
POST /v1/uploadtakes a base64 body behind an AWS API Gateway HTTP API, whose 10 MiB request quota divides by base64's 4/3 inflation to ~7.5 MiB decoded. (The app-level 50 MiBMAX_UPLOAD_MIBis unreachable through the public gateway — don't quote it.) ChatGPT hands over much larger files happily, so expect to meet this; the refusal fires before any bytes are fetched and names the limit. - ChatGPT only. claude.ai injects no file reference into a connector call, and
MCP has nothing in-spec (SEP-2631 is an open draft). On any other host the
model can only fabricate a URL, which the fetch boundary refuses — that refusal
is also the "this host cannot attach files, ask for an
http(s)URL" diagnostic.
Attachment fetch boundary. Fetching a host-supplied URL from a public
endpoint is an SSRF surface, so the fetch is a deny-by-default policy: https:
only; the host must be oaiusercontent.com at the apex or on any subdomain
(OpenAI's own locked domain — where live attachment traffic is served), or
oaisdmntpr<azure-region>.blob.core.windows.net (where it used to be, and where
the oaisdmntpr prefix stays required, because that suffix is multi-tenant
and a suffix-only rule would admit any Azure customer's storage account); no
credentials in the URL, no non-default port; redirects refused; the size cap
enforced from content-length before the body is read and again mid-stream; a
bounded timeout; no headers forwarded; non-2xx refused. Because these hosts are
undocumented vendor infrastructure that changes without notice — it already has
once — the cap, the timeout, and the no-redirect rule hold on their own; the
host check is a filter, not the defence.
Connect each host to exactly one Pipelex server — the local workshop wherever there's a filesystem, the hosted console everywhere else.
| Host | Server | How to connect |
|---|---|---|
| ChatGPT (web) | Hosted console | Apps directory |
| claude.ai (web + mobile) | Hosted console | Connector (custom URL) |
| Claude Desktop (chat mode) | Hosted console | Connector / marketplace plugin |
| Claude Code | Local workshop | claude mcp add, or the pipelex plugin from the pipelex-plugins marketplace (its manifest spawns the workshop) |
| ChatGPT desktop (Codex mode) | Local workshop | ~/.codex/config.toml |
| Cursor | Local workshop | ~/.cursor/mcp.json |
| Claude Desktop (Cowork mode) | Dual — console for consumers, workshop for builders | Connector, or stdio in claude_desktop_config.json |
| Mistral Vibe (TUI) | Local workshop | pending Vibe's MCP mechanics |
| Mistral Vibe (web) | Hosted console | Connector / config |
On views: the hosted console ships the run-graph and run-follow
views, which render on view-capable hosts (ChatGPT, claude.ai, Cowork) and
degrade to text on Claude Code. The local workshop is tools-first — it ships
no views on any host today, so it reports structured results and text
summaries directly. (Codex and Cowork are view-capable hosts and would render
workshop views if local view delivery lands in a later increment.)
A host should be connected to one Pipelex server, never both. Same tool
names on both means a both-installed host has ambiguous routing (nothing
guarantees the model picks the local one), contradictory schemas under identical
names (the workshop accepts { path }, the console rejects it), and doubled tool
registrations for no added capability.
The trap that gets you there without choosing it: a claude.ai Pipelex connector syncs into Claude Code automatically. A user signed into claude.ai with the connector enabled gets the hosted tools in coding sessions alongside a locally-registered workshop. When you run the local workshop, disable the connector for those sessions:
- In Claude Code,
/mcpis the entry point. A connector you haven't signed into is collapsed behind a "Show unused connectors" row (Claude Code v2.1.161+) — expand it to find Pipelex. - Config alternatives: per-project
deniedMcpServersin.claude/settings.json, or globaldisableClaudeAiConnectors: truein user settings.
Full contracts (verdict discipline, _meta channels, view behavior) live in
SPEC.md. The shapes below use SubmittedFileInput from
the two deployments.
// input
{
query?: string; // trimmed, case-insensitive substring over id/name/description
limit?: number; // integer 1..50; default 20
offset?: number; // integer >= 0; default 0
}
// structuredContent — success
{
status: "ok";
total_count: number;
matched_count: number;
returned_count: number;
next_offset: number | null;
methods: Array<{
method_id: string;
name: string;
name_truncated: boolean;
description: string | null;
description_truncated: boolean;
has_source: boolean;
updated_at: string;
}>;
}Both shells expose this read-only, no-view catalog entry point. It fetches the
current API key's complete organization catalog through @pipelex/sdk, filters
and pages locally, and returns a deterministic page sorted by case-insensitive
name then id. Names are bounded to 200 Unicode code points and descriptions to
500; search uses their full values. Empty catalogs, no matches, and offsets past
the end are successful empty results. has_source says only that stored MTHDS
source exists — it is not a valid/runnable verdict.
The projection is deliberately source-free: mthds, Python, stored inputs and
outputs, organization ids, and creator ids never enter structuredContent,
content, _meta, or logs. Upstream listing is not paged: the platform still
returns every full row, and the MCP immediately projects it before producing any
tool output. A malformed row fails the whole result as a non-retryable runtime
contract error rather than returning a misleading partial list.
Name-to-run flow:
mthds_list_methods({ query: "invoice" })
→ choose/disambiguate method_id (do not use a has_source:false draft)
→ mthds_validate({ method_id }) # optional current-content check
→ mthds_inputs_template({ method_id })
→ fill inputs; prepare/upload assets if needed
→ mthds_run({ method_id, inputs })
No method source crosses the conversation in this flow.
// input — at least one of files / method_id
{
files?: SubmittedFileInput[];
method_id?: string; // catalog id (mt_…) of a registered method
include_graph?: boolean;
}
// structuredContent
{
status: "ok" | "error";
is_valid: boolean;
is_runnable: boolean;
pending_signatures: string[];
available_view_specs: Array<"dry_run_graph">;
validation_errors?: unknown[];
errors?: ToolError[];
}The graph (graph_spec) rides the tool result's view-only _meta channel
(_meta.graph_spec) for the run-graph view — never structuredContent, so the
model never pays its tokens. available_view_specs is how the model learns a
view exists to surface; include_graph defaults to true. The MCP content text
carries the human-readable summary. method_id validates a registered method by
its catalog id (fetch-and-forward from the method's current stored content, the
same pattern as mthds_inputs_template); it requires an API key, since the
catalog is org-scoped, and when both files and method_id are supplied the
files win and the id is ignored. The graph view works identically whether the
content came from submitted files or a by-id fetch.
// input — at least one of files / method_id
{
files?: SubmittedFileInput[];
method_id?: string; // catalog id (mt_…) of a registered method
pipe_ref?: string;
explicit?: boolean;
format?: "json" | "toml";
}
// structuredContent
{
status: "ok" | "error";
is_valid: boolean;
pipe_ref?: string;
format?: "json" | "toml";
explicit?: boolean;
inputs?: Record<string, unknown>;
inputs_toml?: string;
validation_errors?: unknown[];
errors?: ToolError[];
}pipe_ref is a qualified domain.pipe_code; omit it to default to the closure's
declared main_pipe. explicit (default true) emits the ceremonial
{concept, content} envelope per input — the declared concept ref plus the
canonical content shape; pass false for the light shape (bare example values).
format (default "json") chooses the template encoding. method_id projects a registered method by its catalog id
(fetch-and-forward from the method's current stored content); it requires an API
key, since the catalog is org-scoped, and when both files and method_id are
supplied the files win and the id is ignored. No Skybridge view — the template is
small structured data the model reads directly, and the content summary repeats
it in a fenced block.
// input — at least one of files / method_id, plus the filled inputs
{
files?: SubmittedFileInput[];
method_id?: string; // catalog id (mt_…) of a registered method
pipe_ref?: string;
inputs: Record<string, unknown>; // the FILLED mthds_inputs_template output
}
// structuredContent
{
status: "ok" | "error";
is_valid: boolean;
pipe_ref?: string; // echoed only when the caller supplied it
inputs?: Record<string, unknown>; // the prepared (rewritten) inputs — ready for mthds_run
uploads?: string[]; // the pipelex-storage:// uris uploaded this call ([] when all pass-through)
errors?: ToolError[];
}Sits between mthds_inputs_template (produces the empty template) and mthds_run
(executes the filled inputs): it makes file-bearing inputs run-ready. The pipe's
declared signature identifies which values are assets; each is uploaded to Pipelex
storage and rewritten to pipelex-storage://. http(s) URLs and existing
pipelex-storage:// references pass through unchanged, so an inputs set that is
already all pass-through can skip this step. Per-deployment asset boundary: the
local workshop uploads local paths, data: URLs, and inline bytes with your
API key; the hosted console is pass-through only and refuses any upload-needing
input up front with an input_domain error at inputs, naming the workshop. No
Skybridge view — the prepared inputs are small structured data the model reads
directly, repeated in the content summary. Unlike the other tools this has no
produced-invalid arm: an unresolvable closure is a no-verdict status: "error"
(recover via mthds_validate / mthds_inputs_template). See SPEC.md →
"Prepare Inputs Scope" for the full contract.
// input — the host fills this in; never construct one yourself
{
attachments: Array<{
download_url: string; // required — the host's signed HTTPS URL
file_id: string; // required — e.g. "sediment://file_0000…"
mime_type?: string;
file_name?: string;
}>;
}
// structuredContent
{
status: "ok" | "error";
is_valid: boolean; // true only when EVERY attachment ingested
attachments?: Array<{
file_id: string;
file_name?: string;
uri?: string; // the pipelex-storage:// reference, on success
content_type?: string;
size?: number; // decoded bytes
error?: ToolError; // per-item failure
}>;
uploads?: string[]; // the successful uris
errors?: ToolError[]; // no-verdict only
}Registered on the hosted console only, and populated by ChatGPT only —
see Chat attachments for the flow, the 7 MiB
cap, and the fetch boundary. The four-field attachment shape is mandated, not
chosen: OpenAI's app review requires exactly these properties with exactly this
required/optional split, and the host's runtime substitution is gated on the
same schema — so a deliberately lenient variant would never be populated. Partial
success is a produced verdict: status: "ok" with is_valid: false, the
successful uploads returned alongside per-item errors rather than discarded.
No Skybridge view — the returned URIs are small structured data the model reads
directly, repeated in the content summary.
Durable (async) method execution on the hosted Pipelex API. mthds_run starts a
run — from submitted files (files?, plus pipe_code? and inputs?), or from a
registered method's catalog id (method_id?, mt_…) — and returns a durable
run_id immediately (never blocks); mthds_run_status is a cheap read of the
coarse lifecycle state; mthds_run_results fetches the terminal outcome (main
output on success, failure message otherwise) along with a compact run-level
usage object — total USD cost (null-aware), tokens, and inference-call count.
The per-pipe rollup and the full per-call record list ride the view-only _meta
(_meta.usage_by_pipe / _meta.tokens_usages) for a future detailed-cost
surface, and usage never appears in the prose. A by-id run executes the method's
current stored content (methods are not versioned) and requires an API key;
when both files and method_id are supplied, the files run and the id is
recorded as run-history linkage on the platform. All run
state lives behind the durable run_id on the platform, so the flow survives
conversation gaps — days later, the same id still answers. On the hosted console,
mthds_run ships the run-follow live-status view; on the workshop these are
plain tools. See SPEC.md → "Run Scope" for the full contract.
A successful catalog page is status: "ok" with counts and methods (there is
no is_valid field). For tools that produce a method verdict, a produced
verdict is always status: "ok" — discriminate on is_valid (and, for
validation, is_runnable); an invalid bundle or unresolvable closure is a
produced is_valid: false verdict, not an error. status: "error" is reserved
for no result/verdict could be produced and carries an errors[] array, each tagged
input_domain (bad request), config (env/auth/unreachable API), or runtime
(server fault), plus a retryable flag. Every errors[] entry's location,
message, and hint are also surfaced in the content text.
The hosted server is a Skybridge app. During early development this repo also
supports the local OSS pipelex-api runner so the MCP can be exercised before
the hosted path is fully wired — temporary; the production target is the hosted
Pipelex API only.
Prerequisites:
- Node.js 24+
- A Pipelex API serving
POST /v1/validateandPOST /v1/build/inputs(a local OSSpipelex-apiduring development) - A WorkOS AuthKit tenant — the console has no keyless mode and refuses to start without one (see below)
The console requires two WorkOS variables. Per-user OAuth is its only auth posture, so the server throws at startup unless both are set:
| Variable | Value |
|---|---|
WORKOS_AUTHKIT_DOMAIN |
the AuthKit domain, e.g. <tenant>.authkit.app |
PIPELEX_MCP_RESOURCE_INDICATOR |
the server origin with a trailing slash — http://localhost:3000/, not .../mcp |
The Resource Indicator must also be registered in the WorkOS dashboard (Connect
→ Configuration), along with Dynamic Client Registration. It becomes the issued
token's aud, and the server verifies it byte-for-byte — registering the /mcp
path or dropping the trailing slash yields tokens that never validate. The
startup check rejects both mistakes with a message naming the fix, rather than
letting every tool call fail later at audience verification.
If you only need to work on the capability core, use make dev-local
instead — the workshop shell shares the same capabilities, authenticates with
a plain PIPELEX_API_KEY, and needs no WorkOS setup at all.
Install dependencies, start the API, then the Skybridge dev server:
npm install
cd ../pipelex-api && make run # serves http://localhost:8081
npm run dev# .env at the repo root (gitignored)
WORKOS_AUTHKIT_DOMAIN=<tenant>.authkit.app
PIPELEX_MCP_RESOURCE_INDICATOR=http://localhost:3000/
PIPELEX_BASE_URL=http://localhost:8081PIPELEX_BASE_URL defaults to the hosted Pipelex API when unset — set it to
http://localhost:8081 to develop against a local runner. PIPELEX_API_KEY has
no effect on the console: the caller's verified OAuth token always overrides
it. .env is dev-only and loaded via nodemon.json
(tsx --env-file-if-exists=.env); it is not watched, so restart the dev server
after editing it.
If port 3000 is taken, Skybridge falls back to another port and prints it — the
Resource Indicator then has to match that port too, both in .env and in the
WorkOS dashboard.
The MCP endpoint is at http://localhost:3000/mcp, with Skybridge DevTools at
http://localhost:3000.
To poke the local workshop stdio server during development:
make dev-local # run the stdio server from TypeScript (tsx)
make inspect-local # open MCP Inspector against itnpm run build # Skybridge app (regenerates .skybridge/views.d.ts first)
npm run build:local # tsup → dist/local/main.js (the npm-distributed bin)
npm run check # lint + format:check + build + build:local + typecheckmthds_validate registers the run-graph view (src/views/run-graph.tsx),
which satisfies Skybridge's "≥1 view entry" production-build requirement. The
Skybridge build scans src/views/ and regenerates .skybridge/views.d.ts (the
view-name registry) as its first step, so npm run check runs build before the
standalone typecheck — the registry must exist for tsc to resolve the
registered view name. The local build follows and prepack rebuilds it, so a
pack/publish can never ship a stale or absent bin.
pipelex-mcp follows Semantic Versioning; version in
package.json is tagged (vX.Y.Z) on release, and npm publish and the Alpic
deploy ship together at one version. See CHANGELOG.md for what
has shipped. 0.1.0 is the first tagged release.