New:Microsoft Teams Notifications Are Now Available in Socket.Learn more →
Get Started

bluet-mcp

Package Overview
Dependencies
Maintainers
1
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

bluet-mcp

MCP-first product memory. Strategy, decisions and gotchas your agents can read and write.

latest
Source
npmnpm
Version
0.5.1
Version published
Weekly downloads
2.2K
Maintainers
1
Weekly downloads
 
Created
Source

Bluet

MCP-first product memory + orchestration for PMs.

Hands structured context, constraints and briefs out to agents over MCP. Multi-product, calm GUI.

What it does, in one line: your agents never start a task blind, and never finish one silently.

  • Before the work — bluet_preflight turns a task into one cited brief: the rules not to break, the decisions that constrain it, the non-goals, the bet it serves, what is still undecided.
  • Before the commit — bluet_check compares the plan or diff with everything decided and names the record each conflict collides with.
  • After the work — bluet_debrief proposes what the session settled. The owner decides; nothing an agent says about itself is written directly.
  • Every sync — recent merged pull requests are compared with the record, and a contradiction is raised as drift.

Honest scope today: the readers are GitHub (hosted) or a local checkout plus gh (bluet-mcp init). Heuristics find candidates; with ANTHROPIC_API_KEY set, a model judges them (Haiku 4.5 for decisions, Sonnet 5 for traps, briefs and contradictions) under a hard monthly cap (BLUET_LLM_CAP_USD, default $20), cached on its inputs. Without a key everything still works on the heuristics alone and says so. Analytics, Linear and other trackers are not read yet.

What it observes it records; what it infers it proposes. A release GitHub published is a fact, so it is written straight in. A pull request that reads like a decision, a warning that looks like a gotcha, and a positioning drafted from a README are all judgments — they arrive as proposals on Now, and accepting is what writes them. A measure and a falsifier are never proposed at all, because nothing in a repository holds them.

Start in one command

In the root of any repository — no account, no keys:

npx bluet-mcp init

It reads the README, the files written for agents (CLAUDE.md, AGENTS.md, CONTRIBUTING.md, the top of docs/) and, if gh is signed in, merged pull requests and issues. It prints what the product is, the traps your agents will now be told about, and the questions only you can answer.

Then install the Claude Code plugin:

claude plugin marketplace add bluet-dev/bluet-plugin
claude plugin install bluet@bluet

The plugin adds the MCP server, a session-start brief (silent in repositories Bluet does not know), a check before every commit (the staged change against what was decided: a blocking conflict stops the commit once and tells Claude why, so it asks you; the same commit run again goes through), a one-time request for a debrief after a session that edited files, and a skill that runs the loop above. npx bluet-mcp brief prints the same brief in a terminal. The plugin's source is plugin/; tool/publish-plugin.sh copies it to the public marketplace repo.

To see Bluet in Claude Code's status line (Bluet · my-app · 12 rules · 3 waiting · 1 caught this week):

npx bluet-mcp statusline --install

It writes a small reader to ~/.bluet/statusline.mjs and sets statusLine in ~/.claude/settings.json, unless you already have one, which it never overwrites. The reader only reads counts the session-start hook caches; it never touches the network.

To tell every agent that opens the repository to use Bluet, even without the plugin, npx bluet-mcp agents-md adds a marked block to AGENTS.md (or CLAUDE.md, when that is the file you use); re-running replaces only that block. It also prints a "Briefed by Bluet" README badge, if you want one.

Linking a machine that already used Bluet locally merges its memory: login adds every local decision and rule the workspace lacks (matched by repository, then name) and changes nothing that is there. npx bluet-mcp merge --dry-run shows what it would add.

Saves. When bluet_check blocks a plan, Bluet records a save ("caught"); if the same product's next check comes back clear within two hours, the agent changed course and the save is "averted". The Product Note on bluet.dev lists them, with what the data says (PostHog, CI on the default branch, the latest production deploy) and what needs you.

Telemetry

bluet-mcp sends anonymous usage counts so we can tell whether Bluet is useful: a random install id, event and tool names, success, outcome counts (how many records a brief cited, a check's verdict), the version and OS. Never content, questions, plans, repository or product names, paths or tokens. The first run says so once, on stderr.

To tell us what is missing: npx bluet-mcp feedback "your words" (sent even with telemetry off, because you asked for it to be sent). Full detail: bluet.dev/privacy.

Turn it off: npx bluet-mcp telemetry off, or set DO_NOT_TRACK=1 / BLUET_TELEMETRY=0. npx bluet-mcp telemetry shows what is sent and whether it is on.

Hosted: bluet.dev

Connect GitHub at bluet.dev and it reads every repository of the product, judges what it finds, and ends setup on the product as it reads it — with an agent briefed on a real open issue from your tracker. The last step issues a workspace token:

npx bluet-mcp login <token>      # stored in ~/.bluet/config.json, mode 600

From then on the same binary, hooks and plugin read and write the hosted memory instead of ~/.bluet (npx bluet-mcp status, npx bluet-mcp logout). Any other MCP client can use the endpoint directly: https://bluet.dev/mcp with Authorization: Bearer <token>. It accepts only a workspace token; it is never open.

Install

No account, no database, no keys. Point an MCP client at it:

{
  "mcpServers": {
    "bluet": { "command": "npx", "args": ["-y", "bluet-mcp"] }
  }
}

Memory is stored locally in ~/.bluet — real Postgres, compiled to WASM via PGlite, so moving to a hosted database later is a row copy rather than a translation. Override the location with BLUET_DB_PATH.

Start by creating a product; every other tool needs one:

bluet_product_create  { "name": "Acme" }
bluet_decision_record { "product": "acme", "title": "…", "decision": "…" }

Set DATABASE_URL to use hosted Postgres instead, and install pg alongside it (it is an optional dependency, so a local install does not pull it in).

Stack

  • GUI — Next.js (App Router) + TypeScript + Tailwind
  • DB — Postgres via pg (DATABASE_URL; Supabase in production)
  • MCP — @modelcontextprotocol/sdk
    • stdio — npm run mcp (local IDE clients)
    • Streamable HTTP + legacy SSE — npm run mcp:http on port 3100 (remote clients)
  • Shared data layer so GUI API routes and MCP hit the same database

Install & run GUI

cd /workspace/bluet   # or your clone path
npm install
npm run dev

Open http://localhost:3000.

Seeding is explicit, not automatic: run npm run seed. It used to run on first boot against an empty database, which is wrong for a shared instance — and wrong on a stranger's machine, which must never receive someone else's demo products.

One-click connect

Sources (/sources) is the catalogue, in three tiers:

  • Connected — what reads today, with how long ago each one was read. GitHub is required; a source nobody has read from in three weeks is marked stale.
  • Found in your code — services named in the selected product's own repo, offered ahead of the catalogue with the file that named them as the reason. Languages and frameworks go to /stack instead: a source is a service Bluet could read evidence out of.
  • Browse all — the full catalogue, searchable and filtered by category.

A Connect button renders only where Bluet actually holds an auth config; everything else reads Not yet connectable rather than opening a window that fails. isConnectable derives from COMPOSIO_PROVIDERS, and a unit test asserts the two can never drift.

Settings (/settings) holds the rest:

  • Auth — Continue with GitHub (Clerk), or Enable BLUET_AUTH_BYPASS when keys are missing.
  • Landscape — Connect for GitHub / PostHog / Vercel (Composio), plus pasted tokens for the providers with no third-party OAuth. After GitHub: select a repo → detections feed Now modules and the Sources middle tier.
  • AI clients — Claude + Grok Bots (Cursor secondary): one-click issues an MCP token and copies HTTP + Bearer config. Stdio is a secondary copy action.
  • Danger zone — deleting a product, behind an impact preview and a typed slug.

Linking a release

PATCH /api/releases { product, id, bet_id?, decision_id? } attaches a release to the bet it serves or the decision that authorised it; null unlinks. Both ids are re-read from the named product first, so one workspace cannot attach its release to another's bet, and a release belonging to a different product 404s.

The selectors are on Timeline, one pair per release. They exist because the drift engine raises "Active bet without linked release" and "Release without linked decision" and used to send people to a Timeline that could only be read — the bet_id and decision_id columns have been in the schema since the first migration with no write path in any surface.

Intent (/intent)

The page a human opens to ask what the product is for, and the page an agent reads over MCP for the same reason. It replaced /timeline, which showed Strategy, Bets, Roadmap and Releases as four boxes and left the reader to work out how they related — which is why the boxes were usually empty. /timeline redirects here.

One continuous argument: a north star, one measure, the bets under it, a Now/Next/Later horizon where every item names the bet it tests, and the versions shipped against them.

Every number on it is counted, never written. A bet's "6 decisions" is a query over decisions.bet_id, not a string in a fixture — a page that asserts its own evidence would break the rule it exists to state. A bet nothing cites reads "Nothing cites this yet", and evidenceLine returns null rather than "0 decisions".

FieldWhere it livesWritten by
North star, standfirst, the one measurestrategies.north_star, measure_*bluet_strategy_set
Claim, reasoning, falsifier, convictionbets.hypothesis, reasoning, falsifier, convictionbluet_bet_upsert
What a bet was drawn fromdecisions.bet_id, gotchas.bet_idbluet_decision_record / bluet_gotcha_add, argument bet (by name or id)
Which bet an item or release servesroadmap_items.bet_id, releases.bet_idPATCH /api/releases, the selectors on Intent

conviction (holding / testing / early) is deliberately not status. Status is lifecycle — active, paused, settled; conviction is how far a bet has been borne out. Folding them together would have made a bet marked "testing" stop matching listBets(id, "active"), which the drift engine depends on. Rows written before the column derive one from status rather than all claiming to be "early".

A bet with no falsifier says so, in the falsifier's own slot: "Not recorded. A bet with no falsifier cannot be lost, only defended."

Pulse (the rail)

A 52px rail pinned to the bottom edge showing one live event at a time. It exists because Bluet's most reassuring fact is invisible: agents are reading the memory right now. The header states that as a count; the rail lets you watch it.

Fed from the same stream as Activity — PulseRail owns no fetching, so the two surfaces cannot disagree about what happened. Tone carries the kind, using the existing signal tokens and never a sixth: purple an agent read, rose drift, amber a gotcha or stale source, sky a decision or feedback, emerald a release or a bet holding.

Rules it keeps: one line, never a stack (the moment it stacks it needs a dismiss affordance, an unread count and a settings panel); dwell floors at 1.6s (faster reads as a stock ticker, the wrong genre for judgment work); role="status" aria-live="polite"; and under prefers-reduced-motion it drops the ping and the slide and holds one line still. Nothing appears only here — every event also lands in Activity, so missing one costs nothing.

⚠️ Until this shipped, events were recorded for MCP calls, chat and sync only, so Activity was a stream in which people never appeared. Recording a decision, adding a gotcha, accepting or dismissing a proposal, and creating or linking a release now each write one (src/lib/record-activity.ts).

One-click connect — what needs setting up

Three tiers, and the difference is Composio's, not ours. Checked against its live API, not its docs (GET /api/v3/toolkits/<slug> → composio_managed_auth_schemes).

All 37 catalogue entries were checked against Composio's own toolkit list (1,556 of them, paged), then each non-managed one re-checked individually — the list endpoint omits auth_config_details, so it reports Vercel as having no auth at all.

Managed — nothing to do (18). GitHub, GitLab, Bitbucket, Linear, Jira, Asana, Productboard, Sentry, PagerDuty, Supabase, Slack, Notion, Confluence, Discord, Teams, Drive, Intercom, Figma. One click today.

Needs an OAuth app of ours (4). Vercel, Railway, Datadog, Snowflake — OAUTH2 exists, Composio holds no app. One click once tool/setup-composio-oauth.mjs has run for each.

API key or bearer only (8). Shortcut, Render, Fly.io, PostHog, Grafana, Neon, CircleCI, Cloudflare — no OAUTH2 mode for Composio to broker.

No Composio toolkit at all (7). Netlify, PlanetScale, dbt, Storybook, Terraform, GitHub Actions, AWS.

⚠️ Our slug is not always Composio's: the catalogue says teams and gdrive, Composio says microsoft_teams and googledrive. The provider list holds our spelling and toComposioSlug translates at the boundary — holding Composio's there made isKnownToComposio("teams") false and rendered both as "Not yet connectable" while they were one click all along.

Our own OAuth app — Vercel. The toolkit supports OAUTH2; Composio just has no app of its own. One click needs a Vercel Integration we register:

  • Create one at vercel.com/dashboard/integrations/console → note its slug, client ID and client secret.
  • Add this redirect URL to it, exactly:
    https://backend.composio.dev/api/v1/auth-apps/add
    
    Composio calls it back there; without it the handshake dies at the callback with nothing useful in the error.
  • Create the auth config:
    COMPOSIO_API_KEY=… CLIENT_ID=… CLIENT_SECRET=… SUBDOMAIN=<integration-slug>      node tool/setup-composio-oauth.mjs vercel
    
    ⚠️ Composio names the third field subdomain and means the Integration slug, not a hostname. A hostname there creates a config that looks fine and never completes.
  • Pin the id it prints: COMPOSIO_VERCEL_AUTH_CONFIG_ID. After this Composio holds the secret and the app never needs it again — which is why a pinned id alone makes isOneClickConnect true.

--dry-run prints what it would send and changes nothing.

Not possible through Composio — PostHog. Its toolkit offers API_KEY and no OAUTH2 mode at all, so there is nothing to supply credentials to. PostHog itself does have OAuth applications; Composio simply does not expose that path, so one-click PostHog would mean building directly against PostHog rather than through the broker. Until then it takes a personal key (phx_…, not the phc_ project key) and a subdomain, collected on Composio's hosted form. The setup script refuses PostHog by name rather than creating something that cannot work.

Sync (GitHub → proposals)

POST /api/sources/sync?product=<slug> reads every repository attached to the product and compares its published releases against what the memory holds. It writes nothing. Each difference becomes a pending proposal on Now:

Observations are recorded; inferences are proposed. A release GitHub has published is a fact — it is written straight in with its evidence, exactly as detections go into Stack without asking. Measured on a real product, the alternative was forty releases whose bodies were all the same 202-character template, thirty-five of them sitting under "Needs your judgment". What reaches the queue is only what Bluet inferred, because an inference can be wrong in a way an observation cannot.

Recorded directly, no approval
A release with no record hereWritten as shipped, with tag, body excerpt and URL as evidence
A record still planned that GitHub publishedFlipped to shipped — delivery is the external system's to own
A shipped record pointing at nothingIts evidence link attached
Proposed for judgmentAccepting does
A warning in CLAUDE.md / AGENTS.mdWrites the gotcha, graded by how absolute the warning is, with the file and section as evidence
A merged pull request that reads like a decisionWrites the decision, with the PR body as rationale and the PR as evidence

Where gotchas come from. CLAUDE.md and AGENTS.md — the files a team already writes for its agents, which is exactly the reader Bluet serves. One in a real repository opens "the things that have actually gone wrong here, so they don't go wrong twice", which is a gotcha list already written in a file nothing read.

Lines are joined back into sentences before scanning, because these files are hard-wrapped at eighty columns and scanning line by line reads fragments. Severity is graded by how absolute the warning is: must never and never are critical, do not and trap are warnings. Code fences, tables, bare links and a file describing itself are skipped. Measured on Captain Kill Switch: 19 traps across three files, including "Never run a kill sweep on the machine driving your session" and "Silence must never read as health."

Which pull requests are raised. Only merged ones — an open PR is an argument still being had, and a closed-unmerged one is an argument somebody lost. A PR has to clear a score built from decision language (decided, instead of, trade-off, ADR), carry a body of at least 180 characters, and not look routine (bump, chore, revert, a bare version number). The proposal says which markers fired, so the judgment can be checked rather than trusted.

Calibrated against 87 merged pull requests across Captain Kill Switch's repositories: 14 raised, 16%. The failure mode is not missing one — it is raising forty, which is the product's own second bet: "teams that dismiss more than half of what we raise are being shown noise wearing a badge."

The conflict rule it implements: Bluet owns intent, external systems own delivery status. A pre-release never flips a record to shipped — a beta is not a delivery.

A repo the connection cannot read is named in the response and in the UI, with the reason — Composio's proxy answers a GitHub 403/404 with a normal response carrying the status, so an unapproved organisation and a repo with no releases are the same silence unless the status is read. Reporting the first as the second tells someone their org approval worked when it did not.

Drafts are skipped. Dedupe is on owner/repo#<release id> and remembers dismissed proposals too, so judging one is final and a poll never re-asks. Onboarding runs this once, so a new product's first Now view holds real proposals rather than an empty queue; Sources → Check for changes runs it again.

⚠️ Until this existed createSyncProposal had no callers at all — the queue was fed by npm run seed and nothing else.

Push briefs

Orchestration out from Agents (/agents):

  • Per-brief Push and bulk Push all
  • POST /api/briefs/push { "product": "pasiv", "agentName": "growth" } or { "product": "pasiv", "all": true }
  • Optional agent webhook via Register agent endpoint (Agents or Settings)
  • Channels: webhook genuinely delivers, by POSTing to a URL you register. mcp_poll (the default) and clipboard only write a brief_deliveries row — there is no queue and nothing is copied; agents read the current brief with bluet_brief_get whether or not Push was ever pressed.
  • MCP: bluet_brief_push (same server logic)

Toast examples: Pushed to growth · webhook 200 · Queued for MCP (agents call bluet_brief_get)

Run MCP — both transports

Stdio (local)

npm run mcp

Used by Claude Desktop, Claude Code, Cursor, Windsurf, VS Code Copilot, Cline, Continue, Goose, etc.

Streamable HTTP (remote)

npm run mcp:http

Listens on 0.0.0.0:3100 by default:

EndpointPurpose
http://127.0.0.1:3100/mcpStreamable HTTP MCP (preferred)
http://127.0.0.1:3100/sseLegacy SSE (older clients)
http://127.0.0.1:3100/messagesLegacy SSE message POST
http://127.0.0.1:3100/healthLiveness

Optional env:

VariableMeaning
BLUET_MCP_HTTP_PORTPort (default 3100)
BLUET_MCP_HTTP_HOSTBind host (default 0.0.0.0)
BLUET_MCP_TOKENIf set, require Authorization: Bearer <token>
BLUET_PUBLIC_URLPublic base URL shown in GUI /sources (e.g. tunnel)
DATABASE_URLRequired. Postgres connection string
BLUET_MCP_ALLOWED_HOSTSComma-separated Host allowlist when you want DNS-rebinding checks

Smoke tests:

npm run mcp:smoke        # DB + tool surface via shared layer
npm run mcp:http:smoke   # spins HTTP server, initialize + tools/list

Connect Claude, ChatGPT, Cursor/Grok, Windsurf, VS Code, Cline, Gemini

Bluet MCP is bidirectional:

  • Read out — bluet_context_get, bluet_products_list, bluet_constraint_list, bluet_*_list / _get, bluet_brief_get, bluet_gotcha_search
  • Write in — bluet_decision_record, bluet_gotcha_add, bluet_brief_set, bluet_ingest, bluet_roadmap_upsert, bluet_release_upsert

The GUI Settings page (/settings) has copy-paste cards for each client. Summary:

Claude Desktop / Claude Code / Cursor / Windsurf / Cline / Continue

Stdio mcpServers entry:

{
  "mcpServers": {
    "bluet": {
      "command": "npm",
      "args": ["run", "mcp"],
      "cwd": "/absolute/path/to/bluet"
    }
  }
}

Or:

{
  "mcpServers": {
    "bluet": {
      "command": "npx",
      "args": ["tsx", "src/mcp/server.ts"],
      "cwd": "/absolute/path/to/bluet"
    }
  }
}

VS Code Copilot

.vscode/mcp.json:

{
  "servers": {
    "bluet": {
      "type": "stdio",
      "command": "npm",
      "args": ["run", "mcp"],
      "cwd": "/absolute/path/to/bluet"
    }
  }
}

ChatGPT / Gemini / remote Grok (HTTP)

  • npm run mcp:http
  • Point the connector at http://127.0.0.1:3100/mcp (or your BLUET_PUBLIC_URL + /mcp)
  • If BLUET_MCP_TOKEN is set, send Authorization: Bearer <token>

HTTP-shaped config some clients accept:

{
  "mcpServers": {
    "bluet": {
      "url": "http://127.0.0.1:3100/mcp"
    }
  }
}

Running it

Two ways, one codebase.

Local, no account — the MCP server runs against PGlite in ~/.bluet, with no hosted database, no sign-in and no broker key:

npm run mcp            # stdio, for an agent config

Override the location with BLUET_DB_PATH. Nothing is seeded: a fresh install starts empty, and bluet_product_create is the first tool to call.

Hosted — set DATABASE_URL and the same code talks to Postgres instead, which is what the web app uses. See .env.example.

MCP tools

ToolDirectionPurpose
bluet_preflightreadCall first, before changing code: given a task (and files), the rules, decisions, non-goals, bet and open questions that bear on it — one cited brief
bluet_checkreadCheck a plan or diff against everything decided; each conflict cites the record it collides with
bluet_debriefwriteAt the end of work: the decisions made and traps hit, proposed for the owner; the summary is checked and a contradiction is raised as drift
bluet_brief_todayreadThe last day: what waits on the owner, recent activity, the rules agents are told, unanswered questions
bluet_proposals_listreadWhat waits for the owner's judgment, with evidence links
bluet_proposal_judgewriteAccept or dismiss one proposal — only on the owner's say-so
bluet_repo_ingestwriteRecord a repository read on the caller's machine (what linked npx bluet-mcp init uploads) — for repositories GitHub will not let bluet.dev read
bluet_products_listreadList all products in Bluet memory
bluet_context_getreadGet full product context (strategy, bets, roadmap, releases, decisions, gotchas, briefs, constraints)
bluet_decision_listreadList decisions for a product
bluet_decision_getreadGet a single decision by id
bluet_gotcha_listreadList gotchas for a product
bluet_gotcha_searchreadSearch gotchas by text query
bluet_drift_listreadList drift: where product memory and the outside world disagree. Each item carries the tool call that resolves it, or why none exists.
bluet_roadmap_listreadList roadmap items for a product
bluet_release_listreadList releases for a product
bluet_brief_getreadGet an agent brief (or all briefs if agent_key omitted)
bluet_constraint_listreadList derived constraints from strategy non-goals/principles and agent briefs
bluet_decision_recordwriteRecord a product decision (optionally superseding a prior one)
bluet_decision_updatewriteEdit fields on an existing product decision without creating a new version
bluet_product_createwriteCreate a product to hold memory. Required before anything else on a fresh install.
bluet_strategy_setwriteSet the product strategy. Supersedes the current one, keeping the previous version linked rather than overwriting it.
bluet_bet_listreadList bets for a product
bluet_bet_upsertwriteCreate or update a bet — a hypothesis the product is testing
bluet_gotcha_addwriteAdd a gotcha / trap / tribal knowledge note
bluet_roadmap_upsertwriteCreate or update a roadmap item
bluet_release_upsertwriteCreate or update a release plan
bluet_brief_setwriteCreate or update an agent brief
bluet_brief_pushwritePush agent brief(s) to endpoints (webhook) and/or queue for MCP poll. Always upserts the canonical brief store and records a delivery row.
bluet_ingestwriteIngest a note/url/payload into Bluet as decision

GUI routes

Route
/Now — bets, next release, decisions, briefs
/timelineStrategy → bets → roadmap → releases
/decisionsList + create
/gotchasList + search + create
/agentsBriefs + Push / Push all + endpoints
/sourcesOne-click auth, OAuth, MCP copy cards

Product switcher (top-left pills) scopes everything to the selected product.

Seed / canary

  • pasiv — canary demo: sharpened strategy, decisions (incl. one open), gotchas, roadmap, releases, growth / quality / engineering briefs, signal pointers. Pending GitHub sync_proposals appear after migrate for Drift on Now.
  • demo (Harbor) — light second product (strategy + decision + gotcha + ops brief) so the switcher feels real.

Re-seed from empty (wipes local DB):

npm run db:reset

db:reset TRUNCATEs every table and re-seeds, so it refuses to run without BLUET_ALLOW_RESET=1:

BLUET_ALLOW_RESET=1 npm run db:reset

After changing src/db/seed.ts, run npm run seed (idempotent) or the reset above.

Auth (Clerk)

Bluet uses Clerk for product GUI auth (App Router).

  • Create an application in the Clerk Dashboard.
  • Copy Publishable key → NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and Secret key → CLERK_SECRET_KEY into .env.local (see .env.example).
  • Optional URLs (defaults work with this repo):
    • NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
    • NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
  • Restart npm run dev. Middleware protects Now / Chat / Decisions / Gotchas / Activity / Sources / Settings and APIs. Public: /sign-in, /sign-up, /api/health.
  • Header shows UserButton when signed in.

Local smoke without keys: set BLUET_AUTH_BYPASS=1. Middleware skips Clerk; a bypass workspace is used. A banner explains the mode. If keys are simply missing (no bypass), the app still boots and shows a Set Clerk keys banner instead of crashing.

Multi-tenant (light): Clerk orgId (preferred) or userId maps to an Bluet workspaces row. First login creates one workspace per user/org and claims orphan products (so seed Pasiv data stays visible).

Sources (Composio Connect)

Pasiv canary flow:

  • Settings → Landscape — one-click Connect for GitHub (Composio-managed OAuth). PostHog and Vercel are verified on connect: Bluet calls the provider and refuses a credential the provider rejects, so the badge cannot go green on a bad key. Neither is pulled from yet — connecting proves the credential and nothing more. GitHub is the exception: see Sync below. If the provider is unreachable the credential is still stored, marked unverified, which is not connected. Vercel can become genuinely one-click with a Vercel Integration's own client id and secret; PostHog cannot — it exposes no third-party OAuth at all.
  • Select a repository after GitHub (Pasiv canary). Bluet scans package.json, vercel.json, env examples, README, etc.
  • Now → Connections — detection-driven modules still appear for services found in the repo (same Composio Connect Link).
  • AI clients — Claude / Grok one-click copy (token + HTTP config).
  • Linear — optional / collapsed.
  • Advanced — collapsed local paste for PostHog/Vercel + agent endpoint register.

Composio setup

  • Sign up at Composio and create an API key.
  • Set COMPOSIO_API_KEY in .env.local (never commit the value).
  • Enable GitHub, PostHog, and Vercel toolkits in the Composio dashboard if needed.
  • Optionally pin auth config IDs:
    • COMPOSIO_GITHUB_AUTH_CONFIG_ID (managed OAuth)
    • COMPOSIO_POSTHOG_AUTH_CONFIG_ID (API_KEY)
    • COMPOSIO_VERCEL_AUTH_CONFIG_ID (API_KEY — no managed OAuth)
    • COMPOSIO_LINEAR_AUTH_CONFIG_ID (optional)
  • On Sources (/sources), Connect GitHub from the catalogue, then pick a repo in Settings. Detected sources appear under Found in your code.
  • Bluet never prints API keys or tokens to logs or the UI after save.

MCP tokens

After login (or bypass), Settings → MCP access can generate a workspace Bearer token (shown once). HTTP MCP (npm run mcp:http) accepts:

  • global BLUET_MCP_TOKEN, or
  • a valid row in mcp_tokens (sha256 hash stored; raw token never persisted)

If neither global nor any workspace tokens exist, HTTP MCP stays open (dev-friendly). Workspace tokens attach workspace context for future scoping.

Stdio MCP (npm run mcp) is unchanged and does not require these tokens.

Copy snippets on Sources use BLUET_PUBLIC_URL for the public /mcp URL.

Environment checklist

VariableRequired for
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEYGUI auth
CLERK_SECRET_KEYGUI auth
BLUET_AUTH_BYPASSLocal smoke without Clerk
COMPOSIO_API_KEYPostHog / GitHub / Vercel / Linear Composio Connect
COMPOSIO_POSTHOG_AUTH_CONFIG_IDOptional pin PostHog auth config
COMPOSIO_GITHUB_AUTH_CONFIG_IDOptional pin GitHub auth config
COMPOSIO_VERCEL_AUTH_CONFIG_IDOptional pin Vercel auth config
COMPOSIO_LINEAR_AUTH_CONFIG_IDOptional pin Linear auth config
BLUET_PUBLIC_URLSources MCP URL snippets
BLUET_MCP_TOKENOptional global HTTP MCP auth

Copy .env.example → .env.local and fill values.

Bluet as source of truth

Bluet owns intent: strategy, bets, decisions, gotchas, and agent briefs/constraints.

Connected systems are evidence. Accepting a proposal updates status and links only — never strategy or decision body text, and that rule is enforced in code rather than only stated here.

Not yet true: nothing currently generates proposals. The structural drift signals (a bet with no release, a shipped release with no decision, a stale connection) are computed and real; the landscape-sourced proposals are seeded fixtures on the canary product.

Conflict rule: Bluet wins on intent; external systems win on delivery status until a Decision is recorded.

Drift (Now)

The Now page shows a Drift panel when pending sync proposals or simple heuristics fire (e.g. active bet with no linked release, release without a decision, stale source connection). Empty state: “No drift — Bluet and landscape agree.”

Demo: open Pasiv on / after migrate — two pending GitHub proposals are seeded for accept/dismiss via POST /api/sync-proposals.

Repo

Intended GitHub path: memory-like-bluet/bluet.

Testing

npm test            # unit + logic (vitest)
npm run e2e         # self-driving browser suite (playwright)
npm run e2e:update  # refresh visual baselines — read the diff before accepting
npm run e2e:report  # open the last HTML report

npm test is offline and deterministic — GitHub reads are mocked, no database.

The browser suite boots the app locally with BLUET_AUTH_BYPASS=1, which is safe because bypassAllowed() refuses on any Vercel deployment and whenever NODE_ENV=production. Specs are read-only and assert against their own e2e-fixture product in the bypass workspace, never real product memory.

Point it at the transaction pooler, not the session pooler:

E2E_DATABASE_URL='postgresql://…@…pooler.supabase.com:6543/postgres' npm run e2e

The session pooler (:5432) holds one Postgres connection per client and exhausts under a concurrent run — the symptom is /api/health returning 503 and every page reporting 500s, which looks exactly like a product bug.

Keywords

mcp

FAQs

Package last updated on 25 Sep 2026

Related posts