Skip to main content
Glama
ga815647

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.