Skip to main content
Glama

Circuit

A visual workflow builder that lives inside a Claude conversation — and runs on the connectors you already have.

You describe an automation in chat. Claude lays the board while you watch, you edit it by hand, and then Claude runs it one step at a time using your Gmail, your Slack, your Airtable. Circuit never asks for a single credential.

MCP MCP Apps License TypeScript


The idea

Every workflow builder makes you learn its canvas: drag a node, open a panel, map a field, repeat. Circuit inverts that. The conversation is the editor. You say what you want; the board appears; you drag a chip only when you disagree with where Claude put it.

Three things make it work.

It streams. While Claude is still emitting the circuit_design call, the host forwards ui/notifications/tool-input-partial. Each time a step object closes in the partial JSON, the board places that chip. The build animation is the model's output arriving — not a canned sequence.

It owns nothing. Circuit has no Gmail integration, no OAuth flow, no API keys, no secrets at rest. A step just names a tool from your tool list — Gmail:search_threads, Slack:send_message, Airtable:create_records_for_table — and Claude makes the call. Anything you can connect to Claude, Circuit can orchestrate on day one. Claude tells Circuit once what it can reach, and from then on a wrong tool name is caught while the board is still being drawn.

It drives. At run time Circuit is a state machine, not an executor. circuit_run hands Claude exactly one directive; Claude does that one thing and reports back with circuit_step; the board updates and the next directive comes out. Branching, filtering and looping never leave the server, so a nine-step workflow with a loop is nine deterministic hops instead of one long improvisation.

        you ──"triage my inbox and draft the easy replies"──▶ Claude
                                                                │
                                        circuit_design (streams) │
                                                                ▼
   ┌───────────────────────────────── the board ─────────────────────────────────┐
   │   watch ─▶ each ─▶ classify ─┬─▶ write ─▶ approve ─▶ send                   │
   │                              ├─▶ find slots                                 │
   │                              └─▶ label                                      │
   └─────────────────────────────────────────────────────────────────────────────┘
                                                                │
                        circuit_run ─▶ one directive at a time  │
                                                                ▼
      Claude calls YOUR connectors ──▶ Gmail · Slack · Calendar · Airtable · …

Related MCP server: Agentled MCP Server

What a session looks like

You: Watch my inbox, work out what each unread message wants, draft replies to the sales ones, and don't send anything without showing me first.

Claude reads circuit_catalog, then makes one circuit_design call. The board draws itself chip by chip as the arguments stream in. Then:

You: Run it.

// circuit_run  →
{ "act": "call_tool", "stepId": "watch",
  "tool": "Gmail:search_threads",
  "arguments": { "q": "in:inbox is:unread -from:me", "max_results": 5 },
  "expect": "Call that tool, then send the whole result back with circuit_step." }

Claude calls its own Gmail tool, reports the threads, and Circuit hands back the next directive — a classify, then a draft, then:

The run stops. You edit the draft in the box and press Approve & continue — the board calls back into the server, the edited text replaces the draft in the run data, and the next directive is the send, with your words in it. Then the loop turns over and does the next thread.

Connectors are checked, not assumed

Naming a tool as a plain string has one sharp edge: get the string wrong and nothing complains until a run is halfway through and a directive points at something Claude cannot call.

circuit_bind closes it. Claude reports the tools it actually has, once; Circuit remembers them and checks every step against that list — the same treatment an unknown step type already gets, extended to connectors.

The step is saved either way — a wrong chip is much easier to see than to describe — but it is marked on the board, listed in the console, and circuit_run refuses to start until it's fixed:

One step names a connector tool you do not have:
  post: 'Slack:post_msg' is not in your tool list — did you mean 'Slack:send_message'?
Fix the tool names, or call circuit_bind again if the user has connected something since.

Suggestions come from bigram similarity weighted toward the connector: a near-miss inside Gmail: scores far above a closer spelling in some other service, because that is almost always what happened. If nothing scores well enough, you get no suggestion rather than a confident wrong one.

Binding is optional. Without it Circuit says plainly that it could not check, and gets out of the way.

Every workflow has a second path

A step fails. The connector 403s, the record isn't there, the API is having a day. Circuit makes that a designed outcome rather than a dead end — each step carries its own policy:

onError.do

What happens

stop (default)

The run halts here and keeps everything it had. Nothing after it ran.

skip

This path ends; the rest of the run — the next loop item, say — carries on.

retry

Circuit hands Claude the same directive again, up to attempts times.

route

The run leaves by the error port, so the board can show a fallback.

A stopped run is not a lost one. It keeps its data, its queue, and its place, so circuit_resume hands out the same directive again once you've fixed the cause — or steps over it with skip: true. That distinction matters: everything before the failure already happened, and re-running the whole workflow would repeat every side effect it had.

The corresponding instruction to Claude is blunt, because this only works if it's honest: report the failure, never substitute a plausible result. A made-up success is the one thing that defeats the entire mechanism.


Quick start

git clone https://github.com/brandononchain/circuit-mcp.git
cd circuit-mcp
npm install
npm run dev            # → http://localhost:8787/mcp

Then in Claude: Settings → Connectors → Add custom connector, paste http://localhost:8787/mcp, and ask for an automation. That is the whole setup — there is nothing to authorize, because Circuit reaches nothing on its own.

For a public URL without a deploy, npx localtunnel --port 8787 or ngrok http 8787 is enough to try it from claude.ai.


The step kit

Twelve types. Four are settled by Circuit itself; the rest become directives.

On the chip

Type

Who does it

What it is

when

trigger.ask

you

Runs when you ask. The default.

every

trigger.schedule

you

Stores a cron. Pair it with one of your scheduled tasks calling circuit_run.

watch

trigger.watch

Claude

Calls a connector tool to look for new items, and starts the run on what it finds.

do

tool.call

Claude

The workhorse. Names a tool from your connectors and the arguments to call it with.

decide

model.classify

Claude

Reads the input and picks one label. The label becomes the output port.

write

model.write

Claude

Drafts text. Sends nothing — a later do step does that.

read

model.extract

Claude

Pulls named fields out of the input as structured data.

only if

logic.filter

Circuit

Stops the path unless the conditions hold.

route

logic.branch

Circuit

Routes on a value it already has.

for each

logic.each

Circuit

Runs everything downstream once per item, with a hard limit.

ask you

gate.approve

you

Parks the run and shows you an editable preview on the board.

report

note.say

Claude

Reports back in the conversation.

The left column is what you actually see. A chip says what it doesmodel.classify is a type name, decide is a thing a person recognizes — and the type only appears when you select the chip.

Nothing here is Gmail-shaped, or Slack-shaped. tool.call is the integration layer, and its surface is whatever you have connected.

Writing a workflow

Steps are plain objects. Wires are named by the port they leave from, which is how a classify fans out:

{
  "id": "intent",
  "type": "model.classify",
  "title": "Read what they want",
  "config": {
    "labels": ["sales", "scheduling", "other"],
    "input": "item",
    "instructions": "Pricing questions are sales."
  },
  "next": [
    { "port": "sales",      "to": "draft" },
    { "port": "scheduling", "to": "slots" },
    { "port": "other",      "to": "label" }
  ]
}

Anywhere a string appears in a step's config, {{…}} reaches into the run's live data and Circuit substitutes it before the directive ever reaches Claude:

Template

Resolves to

{{trigger.subject}}

the payload the run started with

{{steps.draft.text}}

what an earlier step returned

{{item.id}}

the current item inside a logic.each loop

{ "id": "send", "type": "tool.call", "title": "Send the reply",
  "config": { "tool": "Gmail:reply",
              "arguments": { "thread_id": "{{item.id}}", "body": "{{steps.draft.text}}" } } }

A whole-string template keeps its real type — "{{steps.search.threads}}" hands over the array, not its JSON.

The tool surface

Thirteen tools, one capability each. Three are invisible to the model.

Tool

Visible to

What it does

circuit_catalog

model

Step types and their config keys. Read before designing.

circuit_bind

model

Reports the connector tools Claude can actually call.

circuit_tools

model

What Circuit believes you can reach, and when it was told.

circuit_design

model + app

Draws a whole workflow. Streams.

circuit_patch

model + app

Edits an existing board without discarding your layout.

circuit_open · circuit_list

model

Reads.

circuit_run

model + app

Starts a run, returns the first directive.

circuit_step

model + app

Reports a result — or an error — and returns the next directive.

circuit_resume

app + model

Picks a failed run back up, retrying or skipping the step that broke.

circuit_runs

model + app

History.

circuit_arm · circuit_disarm

model + app

Marks a workflow live, and tells Claude how to schedule it.

circuit_answer

app + model

Approve or reject a held gate, with edits.

circuit_move

app only

Persists a drag. Invisible to the model, so a nudge never costs a turn.

circuit_set_enabled · circuit_rename

app + model

Small edits from either side.

circuit_move is the reason the board feels like a canvas rather than a picture: visibility: ["app"] keeps it out of the model's tool list entirely, so dragging a chip writes to the server without waking Claude.


How it fits together

Claude ──tools/call──────────────▶ Circuit MCP ──▶ storage (Supabase or memory)
   ▲                                    │
   │  directives, one at a time         │  ui://circuit/board.html
   │                                    ▼
   └──── calls YOUR connectors ◀── the board ──tools/call (app-only)──▶ Circuit MCP
src/
  graph.ts              workflow + run types, and the board layout
  registry.ts           the twelve step types: schema, summary, and who performs them
  engine/run.ts         the walker — ports, filters, loops, gates, resume
  server.ts             the MCP tool surface and the ui:// resource
  http.ts               one handler: /mcp, /health
  board.ts              what the canvas gets, and what the model reads back
  store/                Store interface · in-memory · Supabase over PostgREST
  app/board.src.html    the board's markup and visual system
  app/app.ts            the board's logic
  app/bridge.ts         an 11 kB MCP Apps view client (the SDK's is 300 kB)
scripts/
  build-app.mjs         inlines the app into a single self-contained ui:// resource
  build-output.mjs      Vercel Build Output API v3
  harness.ts            a local host, running the OFFICIAL AppBridge
  smoke.mjs             drives a full run over the wire

Reading a chip

 ╭─────────────────────────────╮
 │ ○ decide          sales ─ … │   verb · what happened on the last run
 ├─────────────────────────────┤
 │ Read what they want         │   the title Claude wrote
 │ sorts into sales,           │   what it does, in English, from the config
 │ scheduling or other         │
 │ [sales] [scheduling] [other]│   output ports — wires leave from these
 ╰─────────────────────────────╯

The dot in the corner is a pin-1 mark and it carries information: filled means the step is yours, an open ring means Claude performs it, a square means Circuit settles it server-side without asking anyone. The status pill on the right is blank until a run touches the step — a board full of IDLE badges tells you nothing.

Type is Instrument Sans throughout, with Spline Sans Mono reserved for things that really are identifiers: ports, step ids, and the run console. Config summaries are set in the sans, not the mono, so a board reads like a description rather than a stack trace.

Layout

Columns come from a longest-path relaxation, so a chip always sits right of everything that reaches it. A logic.each loop is the one special case: whatever hangs off its done port belongs after the entire loop body, not one column after the loop. Lanes read like the spec — the first wire out of a step carries on in the same lane, later wires fan downward in the order they were written.

Traces route like copper: 45° mitred corners, never a curve. A wire spanning more than two columns drops to a bus below the board and runs there, instead of cutting through the chips in its way.

The view client

src/app/bridge.ts is a ~200-line implementation of the MCP Apps view side — handshake, notifications, tools/call, display mode, auto-resize. It exists because the official @modelcontextprotocol/ext-apps App class carries the whole MCP SDK with it, and that is 300 kB inlined into every resources/read of the board. This is 11 kB.

Hand-rolling a protocol is only defensible if you verify it against the real thing, so scripts/harness.ts runs the official AppBridge host implementation against the app in an iframe. If the handshake, the partial-input stream and the tool callbacks work there, they work in Claude.


Development

npm run dev        # server on :8787, rebuilds the app on change
npm run harness    # builds the app + a local host with real fixtures
open scripts/harness.html      # ?scene=build | run | held | broken | failed   &theme=light
npm run typecheck

With the server running:

node scripts/smoke.mjs

That connects a real MCP client, lists the tools and their _meta.ui bindings, reads the ui:// resource, designs a nine-step workflow with a loop and a gate, then plays the part of Claude — taking each directive, returning a plausible result, and checking that templates resolved, the loop turned over, the gate held, and the out-of-order guard fires.

Adding a step type

One entry in src/registry.ts. kind picks the chip's silkscreen label, actor decides whether Circuit settles it or hands it to Claude, config is the zod schema Claude reads out of circuit_catalog, and summary writes the small line under the chip title. If it is a claude step, add its directive shape in engine/run.ts. Nothing else changes — layout, catalog, board and engine all read from that one object.


Deploy

npm run build                      # → .vercel/output (Build Output API v3)
vercel deploy --prebuilt --prod

Any Node 20 host works; src/vercel.ts is a nine-line adapter over the same handle(req, res) that npm run dev uses.

Variable

What happens without it

SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY

Falls back to in-memory storage — fine locally, lossy on serverless. Run src/store/schema.sql once.

CIRCUIT_TOKENS

/mcp is open and every caller shares the local workspace. Set it before exposing the server: brandon=sometoken,team=othertoken.

There is deliberately no third row. Circuit stores workflows and run history; it never stores anything belonging to a connector.


Security notes

  • No credentials at rest. Circuit holds workflow definitions and run traces. Every side effect happens in Claude's own authorized connector calls, under the user's existing consent.

  • Directives are data, not instructions. A directive names a tool and arguments Circuit resolved from a stored workflow. Claude should refuse a directive naming a tool the user has not connected, and the server refuses out-of-order reports rather than replaying a step.

  • App-only tools cannot be reached by the model. visibility: ["app"] is enforced by the host.

  • The board is sandboxed. The ui:// resource declares an empty connectDomains, so the page can reach nothing but its own origin — Google Fonts is the only external resource, and it degrades to system faces.

Roadmap

  • Connector binding, so a mistyped tool is a design-time error

  • Failure policies, an error port, and a resumable run

  • A test mode that actually withholds writes rather than just labelling the run

  • Wire editing on the canvas — drag from a port to a chip

  • Run replay: scrub a past run and watch the payload move

  • logic.parallel, for fan-out that does not need ordering

  • Workflow inputs, and an export format so boards are shareable

The reasoning behind the order, and what Circuit deliberately will not do, is in ROADMAP.md.

Contributing

Issues and PRs welcome — see CONTRIBUTING.md. The short version: npm run typecheck && node scripts/smoke.mjs should pass, new step types come with a line in the smoke test, and UI changes come with a harness screenshot.

License

MIT — see LICENSE.

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

0Maintainers
No issuesResponse time
Release cycle
0Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • Build and run visual creative-production workflows from your AI agent.

  • Connect your team's living knowledge base — docs, data, issues, CRM — to Claude and ChatGPT.

  • SaaS intelligence for AI agents. 5 unified tools cover 1,000+ services with 91-96% token savings.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/brandononchain/circuit-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server