Skip to main content
Glama
excitingadventures8

affine-chatgpt-mcp-bridge

README.md
# AFFiNE ↔ ChatGPT MCP Bridge


[![CI](https://github.com/excitingadventures8/affine-chatgpt-mcp-bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/excitingadventures8/affine-chatgpt-mcp-bridge/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

**Read-only ChatGPT ↔ AFFiNE Cloud bridge over MCP, running on Cloudflare Workers with GitHub OAuth.**

> [!NOTE]
> This is an independent community project. It is not affiliated with or endorsed by AFFiNE, OpenAI, GitHub, or Cloudflare.


A read-only cloud bridge that lets ChatGPT search and read an **AFFiNE Cloud** workspace over MCP without keeping a Mac, local proxy, or tunnel client running.

**[Руководство: организация пространства, распознавание контекста и работа с ИИ](docs/WORKSPACE_AND_AI_GUIDE_RU.md)**

The bridge runs on **Cloudflare Workers**, authenticates the human user with **GitHub OAuth**, and applies an explicit GitHub username allowlist. Version **1.3.5** includes eight read-only tools:

| Tool | Purpose |
| --- | --- |
| `doc_search` | Search persisted documents through native AFFiNE MCP. |
| `read_document` | Read document text through native AFFiNE MCP. |
| `affine_list_sections` | Read headings, frames, groups and section context. |
| `affine_list_images` | List image blocks with document and section context. |
| `affine_read_image` | Return actual image bytes for visual inspection. |
| `affine_read_structure` | Read tables, rows, fields, tags and references. |
| `affine_write_diagnostics` | Check capabilities and permissions without writing. |
| `affine_sync_diagnostics` | Check WebSocket sync stages without sending changes. |

The last six tools are restricted to the configured owner and require an existing
AFFiNE web-session cookie as well as a successful native-MCP document read.
Additional allowlisted accounts receive only the two text tools.

## Architecture

ChatGPT authenticates with the Worker through GitHub OAuth. The Worker uses
native AFFiNE MCP for text and document authorization. Owner-only tools then use
the fixed AFFiNE Cloud REST, asset and WebSocket endpoints with the stored web
session. Credentials are never accepted as tool arguments or returned to ChatGPT.

No always-on Mac, VPS, Cloudflare Zero Trust subscription, or OpenAI API key is required for this cloud path.

## Status

This project is deliberately **read-only**. It does not create, edit, move, or delete AFFiNE content.

In the September 16, 2026 live check, both selected documents reached WebSocket,
Engine.IO and Socket.IO connection, then timed out at `document_join`. No snapshot
was received. `sync_read_verified`, `ready_for_write` and `persistence_verified`
were all `false`. Reading and `Doc.Update` permission do not prove write delivery
or persistence. See [validation](docs/VALIDATION.md).

This public edition replaces private workspace and document identifiers with
examples. Configure your own owner and workspace before enabling the extra tools.
Publishing this repository does not deploy or change an existing Worker.

It was extracted from a working AFFiNE Cloud ↔ ChatGPT setup and uses Cloudflare's OAuth provider / MCP agent stack. AFFiNE and ChatGPT are evolving products, so UI labels and upstream MCP behavior can change.

## Tested stack

The first public version is pinned to the versions used while building the working bridge:

- `agents` 0.17.4
- `@modelcontextprotocol/sdk` 1.29.0
- `@cloudflare/workers-oauth-provider` 0.8.1
- `wrangler` 4.131.1
- `@cloudflare/workers-types` 5.20260914.1
- `zod` 4.4.3

## Requirements

- AFFiNE Cloud workspace with MCP enabled
- dedicated **read-only** AFFiNE MCP credential
- Cloudflare account with Workers available
- GitHub account
- ChatGPT account/workspace that supports custom remote MCP apps/plugins
- Node.js 24.11+ and npm for deployment

## 1. Clone and install

```bash
git clone https://github.com/excitingadventures8/affine-chatgpt-mcp-bridge.git
cd affine-chatgpt-mcp-bridge
npm ci
npx wrangler login
```

## 2. Create the OAuth KV namespace

```bash
npx wrangler kv namespace create OAUTH_KV
```

Copy the returned namespace ID into `wrangler.jsonc`:

```jsonc
"kv_namespaces": [
  {
    "binding": "OAUTH_KV",
    "id": "YOUR_KV_NAMESPACE_ID"
  }
]
```

Keep the binding name exactly `OAUTH_KV`.

## 3. Deploy once to obtain the Worker URL

Optionally change `name` in `wrangler.jsonc`, then run:

```bash
npm run deploy
```

You will get a URL similar to:

```text
https://affine-chatgpt-mcp-bridge.<your-subdomain>.workers.dev
```

## 4. Create a GitHub OAuth App

GitHub → **Settings → Developer settings → OAuth Apps → New OAuth App**

Use your Worker URL:

```text
Homepage URL:
https://<your-worker>.workers.dev

Authorization callback URL:
https://<your-worker>.workers.dev/callback
```

Save the **Client ID** and generate a **Client Secret**.

Only GitHub's `read:user` scope is requested by the bridge.

## 5. Create a read-only AFFiNE MCP credential

In the target AFFiNE workspace, create a dedicated MCP credential with read-only access.

You need:

```text
AFFiNE MCP URL
https://app.affine.pro/api/workspaces/<workspace-id>/mcp
```

and the full authorization value issued for that credential, normally beginning with:

```text
Bearer ...
```

Do not reuse a write-capable credential.

## 6. Configure Worker secrets

Runtime values are stored as Worker secrets so they are not committed to Git and are not replaced by later `wrangler deploy` operations.

```bash
npx wrangler secret put AFFINE_MCP_URL
npx wrangler secret put AFFINE_AUTH_HEADER
npx wrangler secret put GITHUB_CLIENT_ID
npx wrangler secret put GITHUB_CLIENT_SECRET
npx wrangler secret put ALLOWED_GITHUB_USERS
```

Enter:

- `AFFINE_MCP_URL` — full AFFiNE MCP endpoint
- `AFFINE_AUTH_HEADER` — complete authorization value, normally `Bearer ...`
- `GITHUB_CLIENT_ID` — GitHub OAuth App Client ID
- `GITHUB_CLIENT_SECRET` — GitHub OAuth App Client Secret
- `ALLOWED_GITHUB_USERS` — comma-separated GitHub logins allowed to use this bridge

Example allowlist:

```text
alice,bob
```

The server **fails closed** if `ALLOWED_GITHUB_USERS` is empty.

## 7. Configure optional owner-only tools and deploy

For media, table structure and diagnostics, configure the two public placeholders:

- `OWNER_LOGIN` in `src/affine-media/bridge.mjs`: exact GitHub login returned by OAuth;
- `WORKSPACE_ID` in `src/affine-media/media.mjs`: the same workspace used by `AFFINE_MCP_URL`.

Keep the owner in `ALLOWED_GITHUB_USERS`. Store an existing authorized web session
in the Worker secret `AFFINE_SESSION_COOKIE`; never put cookie contents in source:

```bash
npx wrangler secret put AFFINE_SESSION_COOKIE
npm test
npm run type-check
```

The source placeholders are intentional. Keep deployment-specific edits local;
do not push private workspace identifiers to a public fork. The pilot document
constant is a synthetic test fixture, not a required live document.

For an existing Worker, preserve its current name, KV binding, migrations and
secrets. Do not replace a working `wrangler.jsonc` with this new-install template.

```bash
npm run deploy
```

## 8. Verify OAuth protection

Without an OAuth access token, `/mcp` must reject the request:

```bash
curl -i https://<your-worker>.workers.dev/mcp
```

Expected behavior:

```text
HTTP 401
WWW-Authenticate: Bearer ...
```

Protected-resource metadata should also be available:

```bash
curl https://<your-worker>.workers.dev/.well-known/oauth-protected-resource/mcp
```

The response should identify your `/mcp` endpoint and authorization server.

## 9. Connect ChatGPT

Create a custom remote MCP app/plugin in ChatGPT:

```text
Server URL:
https://<your-worker>.workers.dev/mcp

Authentication:
OAuth
```

During connection:

1. ChatGPT discovers the OAuth metadata.
2. The Worker redirects your browser to GitHub.
3. GitHub authenticates you.
4. The Worker checks your exact GitHub login against `ALLOWED_GITHUB_USERS`.
5. ChatGPT receives an OAuth token for the Worker.
6. The AFFiNE credential remains hidden inside Cloudflare.

For an ordinary allowlisted account the tool scan exposes:

```text
doc_search
read_document
```

For the configured owner it exposes all eight tools listed above. Refresh the
connector's tool catalog after upgrading if the client caches the previous list.

## Example prompts

```text
Find the AFFiNE document "Project Notes" and summarize it.
```

```text
Search my AFFiNE workspace for notes about cinematography.
```

## Local development

Copy the example environment file:

```bash
cp .dev.vars.example .dev.vars
```

Fill it with test credentials, then run:

```bash
npm run dev
```

`.dev.vars` is ignored by Git. Never commit real secrets.

## Security model

- GitHub OAuth authenticates the person connecting the MCP client.
- `ALLOWED_GITHUB_USERS` restricts access to explicit GitHub accounts.
- OAuth state is random, short-lived, stored in Cloudflare KV, and bound to the browser with an HttpOnly/Secure cookie.
- The AFFiNE bearer credential is stored only as a Cloudflare Worker secret.
- The AFFiNE credential should be **read-only**.
- All eight tools are read-only; the six owner-only tools recheck native document access.
- The optional AFFiNE web session is separate from the read-only MCP credential.
- The AFFiNE bearer credential is never returned to ChatGPT or GitHub.

If a credential appears in a public issue, commit, screenshot, chat, or CI log, revoke it immediately and create a new one.

See [SECURITY.md](SECURITY.md).

## Troubleshooting

### AFFiNE returns `401`

Recreate a read-only AFFiNE MCP credential and put the exact working value into `AFFINE_AUTH_HEADER`. Do not shorten, redact, or reconstruct it manually.

### OAuth succeeds but the bridge returns `403`

Check `ALLOWED_GITHUB_USERS`. It must contain the exact GitHub login returned by GitHub, not a display name.

### `OAUTH_KV` errors

The KV **binding name** must remain `OAUTH_KV`, even if the namespace itself has another name.

### npm dependency conflict around `@cloudflare/workers-types`

This repository pins the 5.x Workers types release used with the tested Wrangler version. Older Cloudflare examples may still reference a 4.x package and can trigger `ERESOLVE` with current Wrangler releases.

### ChatGPT does not see the tools

Check these in order:

1. `/mcp` returns `401` without a token, not `404` or `500`.
2. `/.well-known/oauth-protected-resource/mcp` returns JSON metadata.
3. GitHub OAuth callback URL exactly matches `https://<worker>/callback`.
4. your login is in `ALLOWED_GITHUB_USERS`.
5. the AFFiNE MCP credential is still valid.

## Upstream / attribution

The OAuth/MCP architecture is based on Cloudflare's public remote MCP examples and libraries. Cloudflare's `cloudflare/ai` examples are MIT licensed.

- Cloudflare MCP docs: https://developers.cloudflare.com/agents/model-context-protocol/
- Cloudflare GitHub OAuth MCP example: https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-github-oauth
- AFFiNE MCP: https://affine.pro/mcp

## Documentation

- [Русская инструкция по установке](docs/SETUP_RU.md)
- [Пространство, контекст и совместная работа с ИИ](docs/WORKSPACE_AND_AI_GUIDE_RU.md)
- [Validation & privacy checklist](docs/VALIDATION.md)
- [Local folders, backups and upgrades](docs/LOCAL_WORKSPACE_RU.md)

## License

MIT. See [LICENSE](LICENSE).

Third-party notices: [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).