Fellow Aiden brew.link MCP server
by ga815647
README.md
# Fellow Aiden brew.link MCP server
A remote [MCP](https://modelcontextprotocol.io) server on **Cloudflare Workers** that turns a Fellow Aiden brew profile into a shareable **brew.link** URL. It exposes two tools: a local-only profile validator and an explicitly mutating tool that creates a profile through Fellow's private API and returns its share link. It works with native Streamable HTTP clients such as Codex and Claude Code, while retaining the existing Claude.ai browser-connector path.
Built with Cloudflare's [`agents`](https://developers.cloudflare.com/agents/) SDK (`McpAgent`, a Durable Object per session) over the **Streamable HTTP** transport at `/mcp`, plus `@modelcontextprotocol/sdk` and `zod`.
## Tools
| Tool | What it does |
| --- | --- |
| `create_aiden_brew_link` | **External write; require approval.** Validates the profile, then `login → get device → create profile → share` against Fellow's API. Returns the `brew.link` URL (plain text), or a clear error. |
| `validate_aiden_profile` | **Read-only.** Validates the profile against every Aiden constraint **without** calling Fellow's API. Returns `{ "valid": true, "profile": {…} }` or `{ "valid": false, "errors": [...] }`. |
### Profile input schema
All fields are required (except `profileType`, which defaults to `0`). Validated **before** any API call:
| Field | Type | Constraint |
| --- | --- | --- |
| `profileType` | integer | use `0` |
| `title` | string | 1–50 chars, charset `A-Z a-z 0-9` space `! @ # $ % & * - + ? / . , : ) (` |
| `ratio` | number | one of `14, 14.5, 15, … 20` (0.5 steps) |
| `bloomEnabled` | boolean | |
| `bloomRatio` | number | one of `1, 1.5, 2, 2.5, 3` |
| `bloomDuration` | integer | `1–120` (seconds) |
| `bloomTemperature` | number | one of `50, 50.5, … 99` (0.5 steps, °C) |
| `ssPulsesEnabled` | boolean | |
| `ssPulsesNumber` | integer | `1–10` |
| `ssPulsesInterval` | integer | `5–60` (seconds) |
| `ssPulseTemperatures` | number[] | each one of `50…99` by 0.5; **length must equal `ssPulsesNumber`** |
| `batchPulsesEnabled` | boolean | |
| `batchPulsesNumber` | integer | `1–10` |
| `batchPulsesInterval` | integer | `5–60` (seconds) |
| `batchPulseTemperatures` | number[] | each one of `50…99` by 0.5; **length must equal `batchPulsesNumber`** |
## Project layout
```
src/
index.ts MCP server (McpAgent), the two tools, CORS + auth gate + routing
fellow.ts Fellow API client: login → devices → create profile → share (401 re-login retry)
profile.ts zod schema + cross-field length validation
env.d.ts secret typings merged into the generated Env
wrangler.jsonc Worker config (Durable Object binding MCP_OBJECT + migration)
test-tool.ps1 end-to-end handshake test (calls a tool and prints the result)
```
## 1. Install
```powershell
npm install
```
## 2. Set the three secrets
The Worker reads three secrets from `env` — never hardcode them. Set each in your deployed Worker:
```powershell
npx wrangler secret put FELLOW_EMAIL
npx wrangler secret put FELLOW_PASSWORD
npx wrangler secret put MCP_AUTH_TOKEN
```
- `FELLOW_EMAIL` / `FELLOW_PASSWORD` — your Fellow account login (the same one the Fellow app uses).
- `MCP_AUTH_TOKEN` — a long random string **you** choose. It gates the `/mcp` endpoint so only your connector can call it. Generate one with:
```powershell
[Convert]::ToBase64String((1..32 | ForEach-Object { Get-Random -Max 256 }))
```
> **Windows / PowerShell gotcha:** do **not** pipe a value into `wrangler secret put` (e.g. `echo $x | wrangler secret put …`) — PowerShell prepends a UTF‑8 BOM and the stored secret gets a hidden leading character, so logins/auth then fail mysteriously. Instead **run the command with no pipe and paste the value at the interactive prompt**, or pipe through `cmd`: `cmd /c "type secret.txt" | npx wrangler secret put FELLOW_PASSWORD`.
### Local development
For `npm run dev`, copy `.dev.vars.example` to the gitignored `.dev.vars`. Use a distinct, non-production local token. Validation-only tests do not call Fellow, so their Fellow credentials should also remain isolated placeholders:
```
FELLOW_EMAIL=local-validation@example.invalid
FELLOW_PASSWORD=local-validation-placeholder
MCP_AUTH_TOKEN=local-validation-token-not-a-secret
```
```powershell
npm run dev # http://127.0.0.1:8787 (MCP at /mcp, health at /health)
```
Wrangler reads these values from `.dev.vars`; do not supply a real token with `wrangler --var` or expand one into the command line.
## 3. Deploy
```powershell
npm run deploy
```
Wrangler prints the public URL. Your **MCP endpoint** is that URL + `/mcp`:
```
https://fellow-aiden-mcp.<your-workers-subdomain>.workers.dev/mcp
```
(If this is your first Worker, Cloudflare will prompt you to register a free `*.workers.dev` subdomain.)
## 4. Authentication model
The `/mcp` endpoint requires the `MCP_AUTH_TOKEN` shared secret, accepted **two ways** so every client works:
- **`Authorization: Bearer <MCP_AUTH_TOKEN>`** header — used by curl, Codex, Claude Code, and API MCP clients.
- **`?token=<MCP_AUTH_TOKEN>`** query param on the URL — used by **Claude.ai web**, whose "Add custom connector" UI currently has **no field for a bearer token or custom header** (only OAuth client ID/secret). Putting the secret in the URL is the practical way to authenticate the web connector.
`/health` is open (no auth) for liveness checks. Requests with a missing/wrong token get `HTTP 401`.
## 5. Test end-to-end (before wiring a client)
A full test run uses four Streamable HTTP messages (`initialize` → `notifications/initialized` → `tools/list` → `tools/call`) that share an `Mcp-Session-Id`. The included **`test-tool.ps1`** does this for you and verifies each tool's read/write annotation. It reads `MCP_AUTH_TOKEN` inside the PowerShell process, first from the process environment and then from the gitignored `.dev.vars`; it does not accept a token argument and does not launch a child process containing the token.
```powershell
# Local validation with isolated placeholder values in .dev.vars
# (no Fellow API call and no profile created):
./test-tool.ps1 -Url "http://127.0.0.1:8787/mcp" -ValidateOnly
# To validate a deployed endpoint without mutating Fellow, set MCP_AUTH_TOKEN in
# the process environment that launches PowerShell, then use -ValidateOnly:
./test-tool.ps1 -Url "https://fellow-aiden-mcp.<subdomain>.workers.dev/mcp" -ValidateOnly
```
Do not put real tokens in command arguments, `wrangler --var`, curl headers, or test URLs: process listings, shell history, and terminal transcripts may retain them. A full run without `-ValidateOnly` creates a real Fellow profile and must be performed only when that mutation is explicitly intended.
### Raw curl
Health (no auth):
```bash
curl https://fellow-aiden-mcp.<subdomain>.workers.dev/health
```
Wrong/no token is rejected:
```bash
curl -i -X POST "https://fellow-aiden-mcp.<subdomain>.workers.dev/mcp" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
# -> HTTP/1.1 401 Unauthorized
```
For authenticated MCP requests, use `test-tool.ps1` or a client that sources bearer credentials from an environment variable. The raw curl examples intentionally cover only unauthenticated health and rejection checks so a real credential is never expanded into curl's process arguments.
## 6. Add it to Codex
Codex supports remote Streamable HTTP servers and can source a bearer token from the environment. Keep the endpoint free of query secrets and use a dedicated local variable for the connector token:
```toml
# ~/.codex/config.toml
[mcp_servers.fellow-aiden]
url = "https://fellow-aiden-mcp.<your-workers-subdomain>.workers.dev/mcp"
bearer_token_env_var = "FELLOW_AIDEN_MCP_AUTH_TOKEN"
enabled = true
enabled_tools = ["create_aiden_brew_link", "validate_aiden_profile"]
default_tools_approval_mode = "prompt"
[mcp_servers.fellow-aiden.tools.validate_aiden_profile]
approval_mode = "approve"
[mcp_servers.fellow-aiden.tools.create_aiden_brew_link]
approval_mode = "prompt"
```
Set `FELLOW_AIDEN_MCP_AUTH_TOKEN` in the environment that launches Codex to the same value as the Worker's `MCP_AUTH_TOKEN`, then start a fresh Codex session. `validate_aiden_profile` may run automatically because it is local and read-only; `create_aiden_brew_link` remains approval-gated because it writes a real profile and share link.
Do not place the token in the URL or directly in `config.toml`. The query-token compatibility path described below exists only for the browser connector that cannot send a custom bearer header.
## 7. Add it to Claude.ai (web) as a custom connector
1. Go to **Settings → Connectors → Add custom connector**.
2. **Remote MCP server URL**: paste your endpoint **with the token in the URL**:
```
https://fellow-aiden-mcp.<subdomain>.workers.dev/mcp?token=<MCP_AUTH_TOKEN>
```
3. Leave **Advanced settings** (OAuth Client ID/Secret) **blank** — this server uses the URL token, not OAuth.
4. Save. Claude will connect and discover `create_aiden_brew_link` and `validate_aiden_profile`. In a chat, enable the connector and ask Claude to create a brew profile; it returns a `brew.link`.
> Why the token is in the URL: Claude.ai's web connector UI has no bearer-token/header field (only OAuth). The query-param token is the supported way to authenticate. Treat the full URL (with token) as a secret. If you'd rather not put a secret in a URL, the alternative is to implement OAuth via Cloudflare's [`workers-oauth-provider`](https://developers.cloudflare.com/agents/model-context-protocol/authorization/) — a larger change.
### Claude Code / API connector
Use the **header** form with the client's secure environment-variable or secret-store support. Do not embed the bearer value in a CLI argument, checked-in configuration, or URL.
## Scripts
```
npm run dev wrangler dev (local, reads .dev.vars)
npm run deploy wrangler deploy
npm run type-check tsc --noEmit
npm run cf-typegen regenerate worker-configuration.d.ts after wrangler.jsonc changes
```
## Notes & limitations
- **Single brewer assumed.** The brewer id is read from `GET /devices?dataType=real` element `[0]`. Multi-brewer accounts would need a selector.
- **401 handling.** Any Fellow call returning `401` triggers one re-login + retry, per Fellow's API behavior.
- **No persistence.** The server is stateless per call (the Durable Object only backs MCP session transport); nothing about your brews is stored.
- **Unofficial API.** Endpoints/headers are reverse-engineered from the open-source [`9b/fellow-aiden`](https://github.com/9b/fellow-aiden) package and may change without notice.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues