Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .dev.vars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ACCESS_TEAM_DOMAIN=https://your-team.cloudflareaccess.com
ACCESS_AUD=replace-with-access-application-aud
OWNER_EMAIL=you@example.com
ED_API_BASE_URL=https://edstem.org/api/
MCP_WRITE_ENABLED=false
ED_API_TOKEN=replace-with-a-development-token
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ jobs:
bun-version: latest
- run: npm ci
- run: npm run check
- run: npm run worker:types:check
- run: npm run test:node
- run: npm run test:worker
- run: npm run build
- run: npm run pack:smoke

Expand All @@ -46,6 +48,7 @@ jobs:
bun-version: latest
- run: npm ci
- run: npm run check
- run: npm run worker:types:check
- run: npm test
- run: npm run build
- run: npm run pack:check
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ dist/
build/
.venv/
.env
.dev.vars
.dev.vars.*
!.dev.vars.example
.wrangler/
node_modules/
coverage/
.data/
Expand Down
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

TypeScript CLI and MCP access for [Ed Discussion](https://edstem.org).

One Ed implementation powers three adapters:
One Ed implementation powers four adapters:

- `edstem`: compact JSON CLI for agents and scripts
- `edstem-mcp`: local stdio MCP server
- remote MCP: OAuth, encrypted Ed tokens, and SQLite for hosted connectors
- single-owner Worker MCP: Cloudflare Access OAuth with one fixed Ed account

## Install

Expand Down Expand Up @@ -101,6 +102,36 @@ docker compose up -d

The image exposes `/healthz`, `/readyz`, and `/mcp`. SQLite data lives in the `app_data` volume.

### Single-owner Cloudflare Worker

The Worker adapter is deliberately smaller: Cloudflare Access owns OAuth and policy state, while the Worker uses one `ED_API_TOKEN` secret for every authorized request. It has no database, sessions, or application-owned OAuth server. Every authorized connector acts as the configured Ed account.

1. Create a custom hostname for the Worker and a Cloudflare Access **MCP server** application on that exact hostname and `/mcp` path.
2. Enable Access Managed OAuth and Dynamic Client Registration. Add an Allow policy for the exact `OWNER_EMAIL` identity only; MFA or a narrow IdP group is a sensible second factor.
3. Keep the token lifetime short (5–15 minutes). Permit only callback URIs observed from Claude and ChatGPT; do not use broad callback wildcards. Localhost callbacks are needed only for local clients such as Claude Code.
4. Copy `wrangler.jsonc`, replace its placeholder Access team domain, application AUD, and owner email, then set the Ed secret:

```bash
npx wrangler secret put ED_API_TOKEN
npm run worker:types
npm run worker:deploy
```

`ED_API_TOKEN` belongs in a Worker secret, never in `wrangler.jsonc`. `MCP_WRITE_ENABLED` defaults to `false`; set it to `true` only after read-only interoperability passes. The Worker independently verifies the signed `Cf-Access-Jwt-Assertion` signature, issuer, audience, expiry, subject, and owner email, so a direct-origin request still fails closed. `workers.dev` and preview URLs are disabled.

For local development, copy `.dev.vars.example` to `.dev.vars`, use development-only values, and run `npm run worker:dev`. Local requests still need a valid assertion from the configured Access issuer; automated tests generate their own keys and never use production credentials.

#### Compatibility gate

Do not replace the existing Bun remote service until a staging Worker passes all of these checks:

- An unauthenticated `/mcp` request enters an OAuth-discoverable `401` flow, and RFC 8414/RFC 9728 metadata resolves.
- DCR rejects unapproved redirects, PKCE uses S256, RFC 8707 `resource` survives the flow, refresh works, and removing the owner policy blocks new authorization and refresh within the configured lifetime.
- ChatGPT custom connectors, hosted Claude/Desktop, and Claude Code each complete `initialize`, `tools/list`, and `get_user`.
- Each client's actual callback URI is captured and used to narrow the allowlist.

If one client cannot complete Access's standards-based DCR flow, keep the Bun service; a home-grown OAuth shim would be security theater. Rotate the Ed token with `wrangler secret put ED_API_TOKEN`, keep logs free of assertions and credentials, and roll back by routing clients to the unchanged Bun endpoint.

## Development

```bash
Expand All @@ -112,7 +143,7 @@ npm run pack:check
npm run pack:smoke
```

`npm test` runs Vitest for the Node CLI and stdio MCP, then Bun tests for remote OAuth, security, SQLite, and end-to-end reconnect behavior.
`npm test` runs Vitest for the Node CLI and stdio MCP, workerd integration tests for the Cloudflare Worker boundary, then Bun tests for remote OAuth, security, SQLite, and end-to-end reconnect behavior. `npm run build` performs a credential-free Wrangler dry run; only `npm run worker:deploy` publishes the Worker.

## License

Expand Down
Loading
Loading