@neopress/mcp 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,23 +39,72 @@ Endpoints:
39
39
 
40
40
  | Method | Path | Purpose |
41
41
  | ------ | ----------------------------------------- | ------------------------------- |
42
- | `POST` | `/mcp` | MCP Streamable HTTP endpoint |
43
- | `GET` | `/.well-known/oauth-protected-resource/mcp` | OAuth protected resource metadata |
44
- | `GET` | `/.well-known/oauth-authorization-server` | OAuth authorization server metadata |
45
- | `POST` | `/oauth/register` | Dynamic public client registration |
46
- | `GET` | `/oauth/authorize` | Supabase OAuth PKCE redirect |
47
- | `POST` | `/oauth/token` | Supabase PKCE/refresh proxy |
42
+ | `POST` | `/mcp` | MCP Streamable HTTP endpoint (OAuth bearer required) |
43
+ | `GET` | `/.well-known/oauth-protected-resource/mcp` | RFC 9728 protected resource metadata (also served at `/.well-known/oauth-protected-resource`) |
48
44
  | `GET` | `/healthz` | Liveness probe |
49
45
  | `GET` | `/readyz` | Readiness probe |
50
46
 
51
- Remote MCP requires OAuth bearer tokens for `/mcp`. It exposes dynamic client
52
- registration and PKCE token exchange for clients that support remote MCP OAuth.
53
- The access token is then used against the Neopress first-party `/api/v1` API,
54
- so tenant membership remains enforced by the app API.
47
+ The server is a pure Resource Server (RS-only): the OAuth 2.1 Authorization
48
+ Server is Supabase Auth. Clients discover it from `authorization_servers` in
49
+ the protected resource metadata and run dynamic client registration
50
+ (`/auth/v1/oauth/clients/register`), authorization, and token exchange against
51
+ Supabase directly — this server hosts no OAuth routes of its own.
52
+ Unauthenticated `/mcp` requests get a 401 with a `WWW-Authenticate:
53
+ Bearer resource_metadata="..."` pointer. The resource metadata deliberately
54
+ omits `scopes_supported`: clients echo advertised scopes into their authorize
55
+ request, and Supabase rejects custom scopes.
56
+
57
+ Bearer tokens are verified live against Supabase (`GET /auth/v1/user`), so a
58
+ global logout invalidates them immediately. The access token is then used
59
+ against the Neopress first-party `/api/v1` API, so tenant membership remains
60
+ enforced by the app API.
61
+
62
+ ### OAuth consent flow
63
+
64
+ Consent is delegated by Supabase to the Neopress app (Authorization Path
65
+ `https://app.neopress.ai/oauth/consent`, configured in the Supabase dashboard):
66
+
67
+ 1. Client registers via Supabase DCR, then starts
68
+ `GET {SUPABASE_URL}/auth/v1/oauth/authorize` (PKCE, S256).
69
+ 2. Supabase redirects the browser to the app consent page with
70
+ `?authorization_id={id}`.
71
+ 3. The consent page (session-gated on the app origin) fetches the pending
72
+ authorization from `GET {SUPABASE_URL}/auth/v1/oauth/authorizations/{id}`
73
+ using the anon `apikey` plus the signed-in user's access token, and shows
74
+ the client name, account, and a static access description. If the
75
+ authorization was already approved, Supabase returns only `{redirect_url}`
76
+ and the page redirects immediately.
77
+ 4. Approve/deny posts to `POST .../oauth/authorizations/{id}/consent` with
78
+ `{"action":"approve"|"deny"}`; the browser follows the returned
79
+ `redirect_url` back to the client (deny carries `error=access_denied`).
80
+ 5. The client exchanges the code at the Supabase token endpoint. The MCP
81
+ server only ever sees the resulting bearer token.
55
82
 
56
83
  Remote MCP disables `neopress_asset_upload_file` because a hosted server cannot
57
- read the client's local filesystem. Use `neopress_asset_register` for public
58
- URLs, or use local stdio MCP for local file uploads.
84
+ read the client's local filesystem. Two remote-native paths cover the same
85
+ ground without pushing file bytes through the model's context:
86
+
87
+ | You have | Tool | What happens |
88
+ | --- | --- | --- |
89
+ | The bytes, and outbound network of your own | `neopress_asset_presign` → your own PUT → `neopress_asset_register` | You upload straight to storage; nothing crosses the conversation, so size and count are free |
90
+ | A public URL | `neopress_asset_import_url` | The server fetches it and stores our own copy, then registers it |
91
+ | The bytes, but no outbound network | `neopress_asset_upload_base64` | Bytes ride inside the tool call, over the connection your host already has |
92
+
93
+ `neopress_asset_upload_base64` is a deliberate fallback, not a default. Inline
94
+ bytes pass through the model's context (~90K tokens per MB), so it is capped at
95
+ 1MB decoded and should only be used where the presign PUT is refused — a
96
+ sandbox whose egress runs through a domain allowlist, for instance. The cap is
97
+ enforced on decoded bytes, since capping the base64 string would let ~33% more
98
+ through.
99
+
100
+ `neopress_asset_register` on its own only records an external URL — the image
101
+ breaks if the source moves. Prefer `neopress_asset_import_url` unless you
102
+ deliberately want to reference someone else's host.
103
+
104
+ `neopress_asset_import_url` fetches a caller-supplied URL server-side, so it
105
+ resolves and range-checks every hop (including redirects) against private and
106
+ link-local space before connecting, and caps the body at 50MB while streaming.
107
+ See `src/asset-import.ts`.
59
108
 
60
109
  ## Environment
61
110
 
@@ -64,11 +113,11 @@ URLs, or use local stdio MCP for local file uploads.
64
113
  | `PORT` | `4002` | Remote HTTP listen port |
65
114
  | `HOST` | `0.0.0.0` | Remote HTTP listen host |
66
115
  | `NEOPRESS_BASE_URL` | `https://app.neopress.ai` | Neopress API base URL |
67
- | `NEOPRESS_MCP_PUBLIC_URL` | request-derived | Public issuer/resource origin |
116
+ | `NEOPRESS_MCP_PUBLIC_URL` | request-derived in dev | Public resource origin for the protected resource metadata and 401 pointer; required when `NODE_ENV=production` |
117
+ | `UPSTASH_REDIS_REST_URL` | unset (in-memory store) | Upstash Redis for the active site selection store; required together with the token when `NODE_ENV=production` so selections persist across restarts and instances |
118
+ | `UPSTASH_REDIS_REST_TOKEN` | unset (in-memory store) | See `UPSTASH_REDIS_REST_URL` |
68
119
  | `NEOPRESS_MCP_CORS_ORIGINS` | local dev origins | Comma-separated allowlist or `*` |
69
- | `NEOPRESS_MCP_OAUTH_PROVIDER` | `google` | Supabase OAuth provider |
70
- | `NEOPRESS_MCP_OAUTH_CLIENT_SECRET` | generated at boot in non-production | Stable HMAC secret for signed dynamic client IDs; required when `NODE_ENV=production` |
71
- | `NEOPRESS_SUPABASE_URL` | Neopress Supabase project | Supabase Auth base URL |
120
+ | `NEOPRESS_SUPABASE_URL` | Neopress Supabase project | Supabase Auth base URL (authorization server issuer origin) |
72
121
  | `NEOPRESS_SUPABASE_ANON_KEY` | Neopress anon key | Supabase Auth anon key |
73
122
  | `NEOPRESS_MCP_AUTH_MODE` | `oauth` | `none` is dev/test only and rejected in production |
74
123
 
@@ -86,7 +135,6 @@ docker run --rm -p 8080:8080 \
86
135
  Deploy through Artifact Registry / Cloud Run:
87
136
 
88
137
  ```bash
89
- export NEOPRESS_MCP_OAUTH_CLIENT_SECRET="$(openssl rand -base64 32)"
90
138
  export NEOPRESS_MCP_PUBLIC_URL="https://mcp.neopress.ai"
91
139
  pnpm deploy:mcp
92
140
  ```
@@ -94,6 +142,22 @@ pnpm deploy:mcp
94
142
  The deploy script defaults to project `inblog-ver-2`, region `us-west1`,
95
143
  repository `neopress`, and service `neopress-mcp-server`.
96
144
 
145
+ ### CI deploy
146
+
147
+ `.github/workflows/deploy-mcp.yml` deploys automatically on pushes to `main`
148
+ that change the MCP Docker build closure (`packages/mcp`, `packages/sdk`,
149
+ `packages/shared`, `packages/analytics-core`, lockfile/workspace config),
150
+ detected via turbo affected; `workflow_dispatch` always deploys. The job runs
151
+ typecheck/lint, then `pnpm predeploy:mcp` (agent contract gate), authenticates
152
+ with Workload Identity Federation, and executes
153
+ `packages/mcp/scripts/deploy-cloud-run.sh`.
154
+
155
+ The deploy step maps GitHub secrets `UPSTASH_REDIS_REST_URL`,
156
+ `UPSTASH_REDIS_REST_TOKEN` and repository var `NEOPRESS_MCP_PUBLIC_URL` into
157
+ the Cloud Run service environment. Supabase OAuth Server settings (toggle,
158
+ DCR, Authorization Path) are configured in the Supabase dashboard per the
159
+ human runbook in `docs/plan/2026-07-28-remote-mcp-supabase-as-swap.md`.
160
+
97
161
  ## Official Client Drafts
98
162
 
99
163
  Submission-ready drafts live under `integrations/`:
@@ -113,4 +177,4 @@ pnpm dlx @anthropic-ai/mcpb pack packages/mcp/dist/mcpb/neopress
113
177
  ```
114
178
 
115
179
  Do not submit the remote connector drafts until the production MCP origin,
116
- legal/support URLs, review workspace, and real client OAuth scans are verified.
180
+ support URL, review workspace, and real client OAuth scans are verified.