English · 简体中文
A deployable connector template for Meta Muse. Fork it, rename it, point it at your own service. Apache-2.0, no strings.
📖 Read the step-by-step guide · 中文文档
Muse is Meta's personal AI agent. It reaches third-party services through connectors. Building one needs no app registration and no app id — you expose an HTTP API that Muse can read about and call. This is that API, with the parts that are easy to get wrong already done.
This deploys a working skeleton, not a finished connector. The operations are placeholders. What you get is the auth model, the async-job plumbing, the OpenAPI/llms.txt generation and the deploy wiring — so adding a real capability becomes a small, local change.
Worth stating plainly, because connector platforms usually are:
- Deploy anywhere. Vercel, Cloudflare Workers, Docker, a VPS, Fly, Railway. Same source, three adapters, no platform-specific code in the core.
- Storage is a port, not a vendor. In-memory, Cloudflare KV, or any
Upstash-compatible REST endpoint. Implement the four-method
ConnectionStoreinterface and use Postgres, DynamoDB, or a file if you prefer. - No account with us, no API key from us, no service to sign up for.
CONNECTOR_SECRETis a string you generate. There is nothing to call home and no telemetry. - The output is a plain OpenAPI service. Muse reads it, but so can anything
else that speaks OpenAPI — ChatGPT, Claude, Cursor, your own agent. Nothing
here is Muse-specific except the prose conventions in
llms.txt, and you can change those. - Fork freely. Apache-2.0. Rename the package, delete the branding, keep the
NOTICEfile. No attribution required on your deployed service.
What's already handled:
- Per-connection tokens — users never hand Muse your real API key.
- Async job scaffolding — the poll-don't-resubmit pattern, wired into both the OpenAPI document and the prose Muse reads.
- Spend awareness — metered operations are marked so Muse confirms with the person before spending their money.
- One registry — routes,
/openapi.jsonand/llms.txtare all generated from a single list of operations, so they can't drift apart. - Strict config —
PUBLIC_URLandDASHBOARD_URLare required, because they decide where Muse is told to call and where your users are sent to revoke.
Muse ──(hcm_ token)──▶ muse.example.com ──(your credential)──▶ your API
scoped, revocable this connector held server-side
npm install
cp .env.example .env
# set CONNECTOR_SECRET (openssl rand -base64 48), PUBLIC_URL and DASHBOARD_URL
export $(grep -v '^#' .env | xargs)
npm run devSee what Muse will see:
curl -s localhost:8787/openapi.json | jq '.paths | keys'
curl -s localhost:8787/llms.txtMint a token and make a call:
TOKEN=$(curl -s -X POST localhost:8787/v1/link \
-H 'content-type: application/json' \
-H "x-link-secret: $LINK_SECRET" \
-d '{"subject":"you@example.com"}' | jq -r .token)
curl -s localhost:8787/v1/me -H "Authorization: Bearer $TOKEN" | jqRun the tests — they double as the specification:
npm test # 50 tests
npm run typecheckUse the buttons above. Both ask for CONNECTOR_SECRET, PUBLIC_URL and
DASHBOARD_URL.
npm i -g vercel
vercel link
vercel env add CONNECTOR_SECRET # openssl rand -base64 48
vercel env add PUBLIC_URL # https://muse.example.com
vercel env add DASHBOARD_URL # https://example.com
vercel env add KV_REST_API_URL # from Vercel KV (or Upstash)
vercel env add KV_REST_API_TOKEN
vercel deploy --prodvercel.json rewrites every path to api/index.ts so public URLs stay at the
root — Muse must see https://muse.example.com/openapi.json, not
/api/openapi.json.
npm i -g wrangler
wrangler kv namespace create MUSE_KV # then uncomment kv_namespaces in wrangler.jsonc
wrangler secret put CONNECTOR_SECRET
npm run deploy:cfwrangler.jsonc ships minimal on purpose — no hardcoded resource ids and no
custom domain — so the one-click button works for anyone. It sets
nodejs_compat, required because tokens.ts uses node:crypto.
docker build -t muse-connector .
docker run -p 8787:8787 \
-e CONNECTOR_SECRET=... \
-e PUBLIC_URL=https://muse.example.com \
-e DASHBOARD_URL=https://example.com \
muse-connectorWithout KV the connector keeps connections in memory. Locally that's fine. On serverless it is not: each request may land in a fresh isolate, so people get logged out at random and — worse — a revocation only reaches one isolate. The Worker logs a warning when the binding is missing.
Set KV_REST_API_URL / KV_REST_API_TOKEN (Vercel KV or Upstash), or bind
MUSE_KV (Cloudflare KV), and it switches automatically.
Three things to change. Everything else is plumbing.
This is the file you edit. Delete the three demo* operations and add one
Operation per capability:
{
id: "createVideo", // stable; the model may cite it
method: "post",
path: "/v1/videos",
summary: "Generate a video from a prompt",
description: "Starts a generation. Returns a job to track.",
input: { schema: z.object({ prompt: z.string().min(1).max(4000) }) },
spends: { kind: "credits", note: "Costs 1 generation credit." },
async: { pollWith: "getVideo", typicalSeconds: 90 },
handler: ({ provider, connection, body, idempotencyKey }) =>
provider.createVideo(connection, body as any, idempotencyKey),
}That one entry produces the route, the OpenAPI path, the /llms.txt line and
the "do not resubmit" warning — because spends and async are declared once.
Implement ProviderPort against your real API.
providers/placeholder.ts shows the shape and is what you delete.
SERVICE_NAME drives the OpenAPI title, /health and llms.txt. The package
name and repo name are yours to change.
Most of what makes a connector good or bad for Muse is not code.
Generation is asynchronous and costs money. If Muse retries a POST to "check
progress", the person is charged twice. So:
- mark the create operation
asyncand pointpollWithat the read operation - mark it
spendsso the description says so outright - honour
Idempotency-Key(the skeleton does — there's a test for it)
/llms.txt states this in as many words, and the OpenAPI carries
x-long-running, x-poll-operationId and x-requires-confirmation.
Descriptions are read by a model deciding whether to act. "Reads your balance" is weaker than "Read-only. This cannot move funds." State the limit.
Being reachable as a Custom Connector requires nothing from Meta — a user
can point Muse at your URL today. Getting listed in the directory is a
separate process with review; see connector/SUBMISSION.md.
Two things to know going in:
- Meta does not review Custom Connectors, and says so. You own the consequences.
- Meta has not published revenue share, fees, or a review SLA for directory listings.
| Route | Auth | Purpose |
|---|---|---|
GET /health, /healthz |
none | liveness |
GET /openapi.json |
none | what Muse reads to learn the API |
GET /llms.txt, / |
none | the same, in prose |
POST /v1/link |
x-link-secret |
your dashboard → a connection + token |
GET /v1/me |
connector token | who am I / what's granted |
| everything else | connector token | your operations |
Errors are RFC 9457 problem+json.
| Variable | Required | Notes |
|---|---|---|
CONNECTOR_SECRET |
yes | ≥32 chars. Rotating it invalidates all tokens. |
PUBLIC_URL |
yes | the URL Muse calls; goes in the OpenAPI servers entry |
DASHBOARD_URL |
yes | where users revoke access |
SERVICE_NAME |
no | default Muse Connector |
LINK_SECRET |
production | guards POST /v1/link |
KV_REST_API_URL / _TOKEN |
production | or UPSTASH_REDIS_REST_* |
PORT |
no | default 8787 (Docker/Node only) |
PUBLIC_URL and DASHBOARD_URL have no defaults by design. A default would
mean a half-configured deploy tells Muse to call — and sends users to — the
wrong site.
Apache-2.0. Portions derived from 1Claw AI's muse-connector —
see NOTICE.