@neopress/mcp 1.3.1 → 1.5.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.
@@ -18,12 +18,12 @@ to any external directory yet.
18
18
  - Local stdio MCP is ready for agents that can launch `neopress-mcp`.
19
19
  - Remote Streamable HTTP MCP is ready as a standalone server, pending deployment
20
20
  to a stable public HTTPS origin such as `https://mcp.neopress.ai/mcp`.
21
- - Production remote MCP requires a stable `NEOPRESS_MCP_OAUTH_CLIENT_SECRET` so
22
- dynamically registered OAuth clients survive restarts and revisions.
21
+ - Remote MCP is a pure Resource Server (RFC 9728): the OAuth 2.1 Authorization
22
+ Server is Supabase Auth, discovered from `authorization_servers` in the
23
+ protected resource metadata. Clients register and exchange tokens against
24
+ Supabase directly; consent is delegated to the Neopress app consent page.
23
25
  - Tool descriptors include required read/write/destructive/open-world
24
26
  annotations and short invocation status metadata for ChatGPT-compatible hosts.
25
- - Remote OAuth metadata advertises refresh-token continuity through
26
- `offline_access` and supports dynamic public client registration.
27
27
  - Claude Desktop MCPB staging is prepared through
28
28
  `pnpm --filter @neopress/mcp mcpb:stage`.
29
29
 
@@ -35,9 +35,9 @@ to any external directory yet.
35
35
  - Prepare review/test credentials in a non-production Neopress workspace.
36
36
  - Run real client scans in ChatGPT Developer Mode and Claude remote connector
37
37
  settings after deployment.
38
- - Decide whether remote active site selection should stay in-memory or move to
39
- a durable store. Until then, clients should pass `siteId` explicitly or call
40
- `neopress_site_select` at the beginning of a session.
38
+ - Enable the Supabase OAuth Server (toggle, dynamic client registration, and
39
+ the consent Authorization Path) per the human runbook, then run a real
40
+ end-to-end OAuth round-trip.
41
41
 
42
42
  ## Official References Checked
43
43
 
@@ -1,7 +1,8 @@
1
1
  # ChatGPT MCP App Draft
2
2
 
3
- Submission state: draft only. This is prepared for Developer Mode testing and
4
- workspace review, not public submission.
3
+ Submission state: collateral ready — pending human-only items. Listing copy,
4
+ OAuth configuration, and review test cases below are submission-ready; the
5
+ remaining work is the human-action rows in the Submission Checklist.
5
6
 
6
7
  ## App Metadata
7
8
 
@@ -13,9 +14,9 @@ workspace review, not public submission.
13
14
  | MCP endpoint | `https://mcp.neopress.ai/mcp` |
14
15
  | Authentication | OAuth |
15
16
  | Website | `https://app.neopress.ai` |
16
- | Privacy policy | TODO |
17
- | Terms | TODO |
18
- | Support | TODO |
17
+ | Privacy policy | `https://app.neopress.ai/en/privacy` |
18
+ | Terms | `https://app.neopress.ai/en/terms` |
19
+ | Support | TODO (deferred decision) |
19
20
 
20
21
  ## Short Description
21
22
 
@@ -36,15 +37,24 @@ forms, and assets, then publish entries and sites after explicit confirmation.
36
37
 
37
38
  ## OAuth Configuration
38
39
 
39
- - Authorization server: `https://mcp.neopress.ai`
40
+ The MCP endpoint is a pure Resource Server (RFC 9728); the OAuth 2.1
41
+ Authorization Server is Supabase Auth, discovered from `authorization_servers`
42
+ in the protected resource metadata.
43
+
40
44
  - Protected resource: `https://mcp.neopress.ai/mcp`
41
- - Authorization endpoint: `https://mcp.neopress.ai/oauth/authorize`
42
- - Token endpoint: `https://mcp.neopress.ai/oauth/token`
43
- - Dynamic registration endpoint: `https://mcp.neopress.ai/oauth/register`
44
- - Scopes: `neopress:read`, `neopress:write`, `offline_access`
45
+ - Resource metadata: `https://mcp.neopress.ai/.well-known/oauth-protected-resource/mcp`
46
+ - Authorization server issuer: `https://gxrfyjmsmiakirmtzkmv.supabase.co/auth/v1`
47
+ - Authorization server metadata: `https://gxrfyjmsmiakirmtzkmv.supabase.co/.well-known/oauth-authorization-server/auth/v1`
48
+ - Authorization endpoint: `https://gxrfyjmsmiakirmtzkmv.supabase.co/auth/v1/oauth/authorize`
49
+ - Token endpoint: `https://gxrfyjmsmiakirmtzkmv.supabase.co/auth/v1/oauth/token`
50
+ - Dynamic registration endpoint: `https://gxrfyjmsmiakirmtzkmv.supabase.co/auth/v1/oauth/clients/register`
51
+ - Scopes: none advertised — consent shows a static description instead
52
+ ("This app manages sites with your Neopress account")
53
+ - Consent: delegated by Supabase to `https://app.neopress.ai/oauth/consent`,
54
+ gated on the signed-in Neopress session
45
55
  - PKCE: required, S256 only
46
56
  - Token endpoint auth method: `none`
47
- - Refresh grant: supported
57
+ - Refresh grant: supported by the Supabase token endpoint
48
58
 
49
59
  ## Tool Categories
50
60
 
@@ -87,10 +97,94 @@ forms, and assets, then publish entries and sites after explicit confirmation.
87
97
  8. Fetch the live page URLs outside ChatGPT and confirm 200 responses.
88
98
  9. Reopen the app after token expiry and confirm refresh-token continuity.
89
99
 
90
- ## Submission Blockers
91
-
92
- - Deploy and verify `https://mcp.neopress.ai/mcp`.
93
- - Fill legal/support URLs.
94
- - Add a production icon and screenshots if requested by the review flow.
95
- - Prepare review credentials and a throwaway Neopress workspace.
100
+ ## Review Test Cases
101
+
102
+ Submission requires 5 positive and 3 negative test cases. Each case is a
103
+ review prompt plus the expected behavior against the demo account's workspace.
104
+
105
+ ### Positive Cases
106
+
107
+ 1. Site discovery and selection.
108
+ - Prompt: "List my Neopress sites and select the one named Neopress Review
109
+ Demo."
110
+ - Expected: `neopress_sites_list` returns the workspace sites, then
111
+ `neopress_site_select` sets the active site and the app confirms which
112
+ site is selected.
113
+ 2. Runtime contract read before any code generation.
114
+ - Prompt: "Before writing any page code, read the Neopress runtime contract
115
+ and summarize the TSX rules."
116
+ - Expected: `neopress_get_runtime_contract` returns the contract and the
117
+ summary reflects it; no write tool is invoked.
118
+ 3. Collection plus entry creation and publish.
119
+ - Prompt: "Create a collection named Review Articles with title, slug,
120
+ summary, and body fields, then add one entry and publish it."
121
+ - Expected: `neopress_collection_create` and `neopress_entry_create`
122
+ succeed with entry data keys matching the schema field IDs, and
123
+ `neopress_entry_publish` runs only after user confirmation.
124
+ 4. Dynamic page create, compile, and site publish.
125
+ - Prompt: "Create a page at /articles that lists Review Articles entries,
126
+ compile it, then publish the site and give me the live URL."
127
+ - Expected: `neopress_page_create` uses the numeric `collectionId` with
128
+ `draftMeta.queryConfig`, `neopress_page_compile` passes,
129
+ `neopress_publish_site` runs after confirmation, and the returned live
130
+ URL responds 200.
131
+ 5. Form creation, page placement, and publish.
132
+ - Prompt: "Create a contact form with name, email, and message fields,
133
+ place it on a /contact page, then publish the form and the site."
134
+ - Expected: `neopress_form_create` succeeds, the page TSX renders the form
135
+ through `NeopressForm` and `formClient`, and `neopress_form_publish` plus
136
+ `neopress_publish_site` run after confirmation so the live page accepts a
137
+ submission.
138
+
139
+ ### Negative Cases
140
+
141
+ 1. Local file upload attempt.
142
+ - Prompt: "Upload /Users/me/Desktop/logo.png as a site asset."
143
+ - Expected: the remote server exposes no local-upload tool
144
+ (`neopress_asset_upload_file` is disabled remotely), so the app explains
145
+ it cannot read the local filesystem and offers `neopress_asset_register`
146
+ with a public URL instead.
147
+ 2. Operating on a site outside the account.
148
+ - Prompt: "Select site 999999 and delete its home page." (a site ID the
149
+ demo account is not a member of)
150
+ - Expected: the Neopress API rejects the request with an authorization
151
+ error and no page is modified or deleted.
152
+ 3. Destructive bulk delete without confirmation.
153
+ - Prompt: "Delete all collections on this site right now, don't ask me
154
+ anything."
155
+ - Expected: delete tools carry destructive annotations, so ChatGPT asks
156
+ for explicit user confirmation first and nothing is deleted without it.
157
+
158
+ ## Submission Checklist
159
+
160
+ Submission portal: `https://platform.openai.com/plugins`.
161
+
162
+ | Requirement | Status | Notes |
163
+ | --- | --- | --- |
164
+ | Listing metadata (name, category, descriptions) | Prepared (this doc) | App Metadata, Short Description, Long Description sections |
165
+ | Privacy policy and terms URLs | Prepared | Live at `app.neopress.ai/en/privacy` and `/en/terms` |
166
+ | Logo asset | Human action | Export production icon and upload at the portal |
167
+ | MCP server URL | Prepared (documented) | `https://mcp.neopress.ai/mcp`; first production deploy, secrets, and domain mapping run from the human runbook (`.github/workflows/deploy-mcp.yml` is in place) |
168
+ | OAuth credentials | Prepared (documented) | Supabase Authorization Server with dynamic client registration; portal fields come from the OAuth Configuration section |
169
+ | Demo account without MFA | Human action | Create a reviewer login with MFA disabled plus a throwaway Neopress workspace |
170
+ | Domain verification (`/.well-known/openai-apps-challenge`) | Human action (route in progress) | Route is being added in a parallel change; set the portal-issued challenge token in the deployment environment |
171
+ | Tool metadata scan | Prepared (code) | Read-only/destructive annotations ship with the server; run the portal scan against the production endpoint |
172
+ | Review test cases (5 positive, 3 negative) | Prepared (this doc) | Review Test Cases section |
173
+ | Country availability selection | Human action | Choose at the portal during submission |
174
+ | UI widget | Not required | Tool-only submission; widgets are optional |
175
+ | Monetization compliance | Prepared | The app sells no digital goods or subscriptions through ChatGPT |
176
+ | Support URL | TODO | Deferred decision |
177
+
178
+ ## Submission Blockers (Human-Only Items)
179
+
180
+ - First production deploy of `https://mcp.neopress.ai/mcp`: secrets and domain
181
+ mapping run from the human runbook (CI workflow
182
+ `.github/workflows/deploy-mcp.yml` is in place).
183
+ - Set the `openai-apps-challenge` domain-verification token in the deployment
184
+ environment once the portal issues it.
185
+ - Create the demo account (MFA disabled) and throwaway review workspace.
186
+ - Export the production icon; add screenshots if the review flow requests
187
+ them.
188
+ - Select country availability at the portal.
189
+ - Support URL: TODO (deferred decision — legal URLs filled).
96
190
  - Run the full Developer Mode scan once against the production endpoint.
@@ -1,7 +1,8 @@
1
1
  # Claude Remote Connector Draft
2
2
 
3
- Submission state: draft only. Do not submit until the production MCP URL,
4
- privacy/support URLs, and test workspace are finalized.
3
+ Submission state: collateral ready — pending human-only items. Listing copy,
4
+ authentication details, and review prompts below are submission-ready; the
5
+ remaining work is listed under Submission Blockers.
5
6
 
6
7
  ## Listing Metadata
7
8
 
@@ -12,9 +13,9 @@ privacy/support URLs, and test workspace are finalized.
12
13
  | Category | Productivity / Websites / CMS |
13
14
  | Remote MCP URL | `https://mcp.neopress.ai/mcp` |
14
15
  | Website | `https://app.neopress.ai` |
15
- | Support URL | TODO |
16
- | Privacy policy URL | TODO |
17
- | Terms URL | TODO |
16
+ | Support URL | TODO (deferred decision) |
17
+ | Privacy policy URL | `https://app.neopress.ai/en/privacy` |
18
+ | Terms URL | `https://app.neopress.ai/en/terms` |
18
19
 
19
20
  ## Short Description
20
21
 
@@ -36,12 +37,26 @@ Neopress API on every request.
36
37
 
37
38
  ## Authentication
38
39
 
39
- - Type: OAuth 2.1 compatible authorization code with PKCE.
40
- - Registration: dynamic public client registration at `/oauth/register`.
41
- - Authorization metadata: `/.well-known/oauth-authorization-server`.
42
- - Resource metadata: `/.well-known/oauth-protected-resource/mcp`.
43
- - Scopes: `neopress:read`, `neopress:write`, `offline_access`.
44
- - Refresh: supported by `/oauth/token` with `grant_type=refresh_token`.
40
+ The MCP server is a pure Resource Server (RFC 9728). The OAuth 2.1
41
+ Authorization Server is Supabase Auth; clients discover it from
42
+ `authorization_servers` in the resource metadata and run registration,
43
+ authorization, and token exchange against Supabase directly.
44
+
45
+ - Type: OAuth 2.1 authorization code with PKCE (S256).
46
+ - Resource metadata:
47
+ `https://mcp.neopress.ai/.well-known/oauth-protected-resource/mcp`.
48
+ - Authorization server metadata:
49
+ `https://gxrfyjmsmiakirmtzkmv.supabase.co/.well-known/oauth-authorization-server/auth/v1`.
50
+ - Registration: Supabase dynamic client registration at
51
+ `https://gxrfyjmsmiakirmtzkmv.supabase.co/auth/v1/oauth/clients/register`.
52
+ - Scopes: none advertised (`scopes_supported` is intentionally absent from the
53
+ resource metadata). The consent page shows a static description instead:
54
+ "This app manages sites with your Neopress account."
55
+ - Consent: Supabase delegates authorization to the Neopress app consent page
56
+ (`https://app.neopress.ai/oauth/consent`), gated on the signed-in Neopress
57
+ session.
58
+ - Refresh: supported by the Supabase token endpoint
59
+ (`grant_type=refresh_token`).
45
60
 
46
61
  ## Tool Safety Summary
47
62
 
@@ -63,11 +78,33 @@ Neopress API on every request.
63
78
  5. "Create a dynamic page for the entry, compile it, publish the site, and show the live URL."
64
79
  6. "Create a simple dashboard-managed contact form, publish it, add it to a page, and publish the site."
65
80
 
81
+ ## Submission Process
82
+
83
+ - Submission portal:
84
+ `https://claude.ai/admin-settings/directory/submissions/new`.
85
+ - Organization requirement: submitting requires a Claude Team or Enterprise
86
+ organization, and only an Owner of the primary organization can submit.
87
+ - Review flow: an automated policy scan runs on submission; passing it lists
88
+ the connector in the directory as a Community connector.
89
+ - Verified status: Community connectors are escalated to Verified based on
90
+ usage — there is no separate application to prepare for it here.
91
+ - Review contact: `mcp-review@anthropic.com`.
92
+
66
93
  ## Submission Blockers
67
94
 
68
- - Deploy remote MCP to a stable HTTPS origin.
95
+ Remaining human-only items:
96
+
97
+ - First production deploy of `https://mcp.neopress.ai/mcp`: secrets and domain
98
+ mapping run from the human runbook (CI workflow
99
+ `.github/workflows/deploy-mcp.yml` is in place).
100
+ - Submitter access: an Owner seat in the primary Claude Team/Enterprise
101
+ organization must perform the submission.
102
+ - Prepare a non-production test site and review credentials.
69
103
  - Verify OAuth refresh behavior in Claude after a token expiry window.
70
- - Replace TODO legal/support URLs.
71
- - Prepare a non-production test site and test credentials for review.
72
- - Capture any required screenshots or screen recordings.
73
- - Decide whether to persist active site selection outside process memory.
104
+ - Support URL: TODO (deferred decision — legal URLs filled).
105
+ - Capture screenshots or screen recordings if the review flow requests them.
106
+
107
+ Resolved:
108
+
109
+ - Active site selection persistence — stored in Upstash Redis with a 30-day
110
+ TTL, no longer process memory.
@@ -0,0 +1,61 @@
1
+ # Neopress 리모트 MCP — 등재까지 사람 가이드
2
+
3
+ 작성 2026-07-28. 2026-09-07 코드 기준: RS-only 전환은 이미 main에 반영돼 있다. 아래 인프라 절차는 신규 환경 구성·재검증용이며 현재 배포나 등재 완료를 의미하지 않는다.
4
+
5
+ 외부 서비스의 제출 자격·화면·심사 정책은 7월 조사 메모다. 실제 제출 시 각 서비스 공식 문서에서 다시 확인한다. 제출 초안은 `claude-remote/connector-draft.md`, `chatgpt/app-draft.md`에 있고, support URL·리뷰 환경·실제 OAuth 왕복 확인은 별도 완료가 필요하다.
6
+
7
+ ## 이미 끝나 있는 것 (코드·문서 — 사람 액션 불요)
8
+
9
+ - 리모트 MCP 서버(RS-only, Supabase OAuth 2.1 Server 기반), 동의 페이지, Redis 영속화, CI 배포 워크플로우, 도메인 검증 라우트(`/.well-known/openai-apps-challenge`), `pnpm check:agent-contracts` 계약 게이트
10
+ - 제출 컬래터럴: 리스팅 문구, OAuth 설정 명세, 툴 안전성 요약, 리뷰 테스트 프롬프트/케이스(양성 5·음성 3), privacy(`https://app.neopress.ai/en/privacy`)·terms(`/en/terms`) URL
11
+
12
+ ## 지금 바로 시작해두면 좋은 것 (리드타임 있음 — 순서 무관)
13
+
14
+ | # | 액션 | 왜 먼저 |
15
+ |---|------|--------|
16
+ | A | **Claude Team/Enterprise 조직 확인** — 제출 포털(`claude.ai/admin-settings/directory/submissions/new`)은 Team/Enterprise 조직의 **기본 조직 Owner**만 접근 가능. 개인 플랜엔 메뉴 자체가 없음 | 조직이 없으면 개설/업그레이드에 시간 소요 |
17
+ | B | **OpenAI 조직 identity 검증** — platform.openai.com에서 개발자/사업자 인증(미검증 = 리젝). 제출엔 Apps Management write 권한 필요 | 검증 심사가 며칠 걸릴 수 있음 |
18
+
19
+ ## Phase 1 — 인프라 (머지 후, 약 30–60분)
20
+
21
+ 1. 배포할 main 커밋과 대상 환경을 확인한다. RS-only 코드 전환은 완료돼 있다.
22
+ 2. **Upstash Redis**: 인스턴스 준비(기존 재사용 가능) → `UPSTASH_REDIS_REST_URL`/`UPSTASH_REDIS_REST_TOKEN` 확보.
23
+ 3. **GitHub 설정**: 레포 Settings → Secrets에 `UPSTASH_REDIS_REST_URL`, `UPSTASH_REDIS_REST_TOKEN` (+ 나중에 `OPENAI_APPS_CHALLENGE_TOKEN`), Variables에 `NEOPRESS_MCP_PUBLIC_URL=https://mcp.neopress.ai`. ⚠️ 머지 직후 첫 자동 CI run은 secrets 없으면 실패가 정상 — 등록 후 재실행.
24
+ 4. **Supabase 대시보드** (프로젝트 gxrfyjmsmiakirmtzkmv): Authentication → **OAuth Server 활성 + Dynamic Client Registration 활성 + Authorization Path = `https://app.neopress.ai/oauth/consent`**.
25
+ 5. **배포**: Actions → deploy-mcp → Run workflow (또는 로컬 `pnpm deploy:mcp`).
26
+ 6. **도메인**: `gcloud run domain-mappings create --service neopress-mcp-server --domain mcp.neopress.ai --region us-west1 --project inblog-ver-2` → 안내된 CNAME을 DNS에 등록.
27
+ 7. **확인**: `curl https://mcp.neopress.ai/healthz`(200), `curl https://gxrfyjmsmiakirmtzkmv.supabase.co/.well-known/oauth-authorization-server/auth/v1`(feature_disabled가 아닌 JSON), `curl https://mcp.neopress.ai/.well-known/oauth-protected-resource/mcp`.
28
+
29
+ ## Phase 2 — 실동작 검증 스파이크 (토글 후, 1회)
30
+
31
+ 이 단계는 저(에이전트)에게 "스파이크 돌려줘"라고 시키면 됨 — 사람은 결과 확인만.
32
+
33
+ - MCP inspector(`npx @modelcontextprotocol/inspector`)로 `https://mcp.neopress.ai/mcp` 연결 → DCR→동의 페이지→토큰→툴 호출 E2E 왕복
34
+ - OAuth 발급 토큰의 서명 alg 실측(ES256이면 JWKS 로컬 검증 전환 후보 — 등재 전 게이트)
35
+ - DCR register 엔드포인트 rate limit 관찰 + 테스트 클라이언트 정리
36
+ - Claude Code(`claude mcp add --transport http neopress https://mcp.neopress.ai/mcp`)와 ChatGPT Developer mode(Settings → Connectors → Advanced)로 각 클라이언트 실연결 확인
37
+
38
+ ## Phase 3 — 사람만 할 수 있는 결정·준비 2건
39
+
40
+ 1. **Support URL 확정** (현재 유일하게 비어 있는 리스팅 필드): 권장 = `mailto:support@neopress.ai`(수신 가능해야 함) 또는 Crisp 채팅이 뜨는 `https://app.neopress.ai`. 결정 후 저에게 알려주면 드래프트 2종에 반영.
41
+ 2. **리뷰어용 데모 계정 + 테스트 사이트**: 프로덕션에 리뷰 전용 계정 생성(**MFA 없이 로그인 가능해야 함** — OpenAI 요건), 테스트 사이트 1개 연결(실사용자 데이터 없는 것). 크리덴셜은 제출 폼에만 입력.
42
+
43
+ ## Phase 4 — Claude 커넥터 제출 (약 15분)
44
+
45
+ 1. Team/Enterprise **Owner 계정**으로 `claude.ai/admin-settings/directory/submissions/new` 접속.
46
+ 2. `connector-draft.md`의 값 그대로 입력: 이름/카테고리/설명/MCP URL(`https://mcp.neopress.ai/mcp`)/웹사이트/privacy/terms/support + 리뷰 테스트 프롬프트.
47
+ 3. 제출 → 자동 정책 스캔 통과 시 **Community 커넥터로 즉시 등재**됨(별도 심사 대기 없음). 이후 사용량 기반으로 Anthropic이 Verified 리뷰로 자동 에스컬레이션. 문의: mcp-review@anthropic.com.
48
+
49
+ ## Phase 5 — ChatGPT 앱 제출 (약 30분 + 심사 대기)
50
+
51
+ 1. 사전: Phase 2에서 Developer mode 연결이 이미 확인된 상태여야 함.
52
+ 2. **platform.openai.com/plugins**에서 제출 시작 → `app-draft.md`의 리스팅 값 입력(로고 이미지는 사람 준비 — 기존 브랜드 자산 사용).
53
+ 3. **도메인 검증**: 폼이 발급하는 challenge 토큰을 받아 → GitHub Secrets `OPENAI_APPS_CHALLENGE_TOKEN`에 등록 → deploy-mcp 재실행 → `curl https://mcp.neopress.ai/.well-known/openai-apps-challenge`가 토큰을 반환하는지 확인 → 폼에서 검증 진행.
54
+ 4. 데모 계정 크리덴셜, 테스트 케이스(app-draft의 양성 5·음성 3), 국가 가용성 입력 → 제출.
55
+ 5. 심사는 수동, 기간 미공표. ⚠️ 앱 내 디지털 상품/구독 판매 금지 — Neopress 유료 플랜 판매를 앱 안에서 유도하지 말 것(외부 웹사이트 결제는 무관).
56
+
57
+ ## 등재 후·병행 트랙 (원하면 저에게 시키면 되는 것)
58
+
59
+ - JWKS 로컬 검증 전환(Phase 2 실측이 ES256이면), DCR 레지스트리 정리 runbook 작성
60
+ - 셀프서브 등재: GitHub MCP Registry(`mcp-publisher` + DNS 검증), Cursor 딥링크+cursor.directory, Gemini CLI extension — 심사 없이 가능
61
+ - Claude Code **Plugin Directory**(스킬+MCP 번들, 공개 GitHub repo 필수)는 별도 트랙 — `packages/ai-skills`를 공개 repo로 재포장할지 결정 필요
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neopress/mcp",
3
- "version": "1.3.1",
3
+ "version": "1.5.0",
4
4
  "description": "Neopress MCP server for first-party AI agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,6 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@modelcontextprotocol/sdk": "^1.25.2",
32
+ "@upstash/redis": "^1.34.0",
32
33
  "zod": "^4.1.12"
33
34
  },
34
35
  "devDependencies": {
@@ -36,7 +37,9 @@
36
37
  "tsdown": "^0.22.14",
37
38
  "typescript": "npm:@typescript/typescript6@6.0.2",
38
39
  "vitest": "^4.1.5",
39
- "@neopress/sdk": "0.4.0"
40
+ "@neopress/logging": "0.0.0",
41
+ "@neopress/sdk": "0.4.0",
42
+ "@neopress/shared": "0.0.0"
40
43
  },
41
44
  "scripts": {
42
45
  "build": "NODE_OPTIONS=--max-old-space-size=4096 tsdown && node ./scripts/check-dist-contract.mjs",
@@ -3,14 +3,15 @@
3
3
  // for the rationale; the file-set invariant matters more here because
4
4
  // prepare-mcpb-package.mjs copies dist/bin.cjs alone into the Claude Desktop bundle,
5
5
  // so a shared chunk would produce an extension that breaks at runtime.
6
- import { readFileSync, readdirSync } from 'node:fs';
7
- import path from 'node:path';
6
+ import { readFileSync, readdirSync } from "node:fs";
7
+ import { builtinModules } from "node:module";
8
+ import path from "node:path";
8
9
 
9
- const packageRoot = path.resolve(import.meta.dirname, '..');
10
- const distDir = path.join(packageRoot, 'dist');
11
- const packageJson = JSON.parse(readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
10
+ const packageRoot = path.resolve(import.meta.dirname, "..");
11
+ const distDir = path.join(packageRoot, "dist");
12
+ const packageJson = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8"));
12
13
 
13
- const expectedFiles = ['bin.cjs', 'http-bin.cjs', 'index.d.ts', 'index.js'];
14
+ const expectedFiles = ["bin.cjs", "http-bin.cjs", "index.d.ts", "index.js"];
14
15
  const allowedBareImports = new Set(Object.keys(packageJson.dependencies ?? {}));
15
16
  // See the cli script: tsdown inlines a leaked third-party type instead of leaving a
16
17
  // dangling import, turning a loud failure into silent bloat. Current output is ~26KB.
@@ -23,13 +24,13 @@ const unexpected = actualFiles.filter((f) => !expectedFiles.includes(f));
23
24
  const missing = expectedFiles.filter((f) => !actualFiles.includes(f));
24
25
  if (unexpected.length > 0) {
25
26
  errors.push(
26
- `dist has unexpected files: ${unexpected.join(', ')}\n` +
27
+ `dist has unexpected files: ${unexpected.join(", ")}\n` +
27
28
  ` Likely code splitting — give each entry its own config object in tsdown.config.ts.\n` +
28
- ` dist/bin.cjs must stay self-contained: prepare-mcpb-package.mjs ships it alone.`
29
+ ` dist/bin.cjs must stay self-contained: prepare-mcpb-package.mjs ships it alone.`,
29
30
  );
30
31
  }
31
32
  if (missing.length > 0) {
32
- errors.push(`dist is missing: ${missing.join(', ')}`);
33
+ errors.push(`dist is missing: ${missing.join(", ")}`);
33
34
  }
34
35
 
35
36
  // Every emitted bundle must have the workspace packages inlined. tsdown reports an
@@ -37,30 +38,57 @@ if (missing.length > 0) {
37
38
  // which would publish a binary carrying a bare require("@neopress/sdk") that no consumer
38
39
  // can resolve. Checking the .d.ts alone does not see this.
39
40
  for (const file of actualFiles.filter((f) => /\.(cjs|js|mjs)$/.test(f))) {
40
- const bundle = readFileSync(path.join(distDir, file), 'utf8');
41
+ const bundle = readFileSync(path.join(distDir, file), "utf8");
41
42
  const leaked = [
42
43
  ...new Set(
43
- [...bundle.matchAll(/(?:require\(|from\s*)["'](@neopress\/[^"']+)["']/g)].map((m) => m[1])
44
+ [...bundle.matchAll(/(?:require\(|from\s*)["'](@neopress\/[^"']+)["']/g)].map((m) => m[1]),
44
45
  ),
45
46
  ];
46
47
  if (leaked.length > 0) {
47
48
  errors.push(
48
- `dist/${file} still imports workspace packages at runtime: ${leaked.join(', ')}\n` +
49
+ `dist/${file} still imports workspace packages at runtime: ${leaked.join(", ")}\n` +
49
50
  ` They must be bundled. tsdown only warns on unresolved imports, so a bare\n` +
50
51
  ` require would ship — sdk exports src directly, so this means the import failed\n` +
51
- ` to resolve (renamed source file or export-map drift), not a missing prebuild.`
52
+ ` to resolve (renamed source file or export-map drift), not a missing prebuild.`,
52
53
  );
53
54
  }
55
+
56
+ // The .cjs binaries run without node_modules (the MCPB bundle ships bin.cjs
57
+ // alone; the Docker runtime stage copies dist/ alone), so a declared
58
+ // dependency left external is just as fatal there as a workspace leak — the
59
+ // allowedBareImports pass above only protects the library entry.
60
+ if (file.endsWith(".cjs")) {
61
+ // The backtick lookbehind skips require literals inside codegen templates:
62
+ // ajv's standalone-validator generator embeds `require("ajv/dist/...")` as
63
+ // template text that never executes (the modules themselves are inlined).
64
+ const bare = [
65
+ ...new Set(
66
+ [...bundle.matchAll(/(?<!`)require\(\s*["']([^"'./][^"']*)["']\s*\)/g)]
67
+ .map((m) => m[1])
68
+ .filter((specifier) => {
69
+ const name = specifier.replace(/^node:/, "").split("/")[0];
70
+ return !builtinModules.includes(name);
71
+ }),
72
+ ),
73
+ ];
74
+ if (bare.length > 0) {
75
+ errors.push(
76
+ `dist/${file} requires packages at runtime: ${bare.join(", ")}\n` +
77
+ ` This binary ships without node_modules — add the package to that entry's\n` +
78
+ ` deps.alwaysBundle in tsdown.config.ts.`,
79
+ );
80
+ }
81
+ }
54
82
  }
55
83
 
56
- const declarations = actualFiles.includes('index.d.ts')
57
- ? readFileSync(path.join(distDir, 'index.d.ts'), 'utf8')
58
- : '';
84
+ const declarations = actualFiles.includes("index.d.ts")
85
+ ? readFileSync(path.join(distDir, "index.d.ts"), "utf8")
86
+ : "";
59
87
  if (declarations.length > maxDeclarationBytes) {
60
88
  errors.push(
61
89
  `dist/index.d.ts is ${Math.round(declarations.length / 1000)}KB, over the ${Math.round(maxDeclarationBytes / 1000)}KB ceiling\n` +
62
90
  ` A third-party type tree was probably inlined. Narrow the public surface\n` +
63
- ` (@internal + stripInternal) rather than raising this number.`
91
+ ` (@internal + stripInternal) rather than raising this number.`,
64
92
  );
65
93
  }
66
94
  const specifiers = [
@@ -70,23 +98,51 @@ const specifiers = [
70
98
  ...[...declarations.matchAll(/\bimport\s*\(\s*["']([^"']+)["']\s*\)/g)].map((m) => m[1]),
71
99
  ];
72
100
  const unresolvable = [...new Set(specifiers)].filter((specifier) => {
73
- if (specifier.startsWith('node:')) return false;
74
- const packageName = specifier.startsWith('@')
75
- ? specifier.split('/').slice(0, 2).join('/')
76
- : specifier.split('/')[0];
101
+ if (specifier.startsWith("node:")) return false;
102
+ const packageName = specifier.startsWith("@")
103
+ ? specifier.split("/").slice(0, 2).join("/")
104
+ : specifier.split("/")[0];
77
105
  return !allowedBareImports.has(packageName);
78
106
  });
79
107
  if (unresolvable.length > 0) {
80
108
  errors.push(
81
- `dist/index.d.ts imports that consumers cannot resolve: ${unresolvable.join(', ')}\n` +
109
+ `dist/index.d.ts imports that consumers cannot resolve: ${unresolvable.join(", ")}\n` +
82
110
  ` Allowed: node: builtins and this package's own dependencies.\n` +
83
- ` Relative imports mean a type was not inlined; bare ones mean an undeclared dependency leaked.`
111
+ ` Relative imports mean a type was not inlined; bare ones mean an undeclared dependency leaked.`,
112
+ );
113
+ }
114
+
115
+ // ── 3. The SDK client stays HTTP-only ───────────────────────────────────────
116
+ // `NeopressClientOptions` is inlined into this .d.ts because the public surface
117
+ // returns a `NeopressClient`. That makes the emitted declaration the enforceable
118
+ // record of the option bag — assert the interface is still here (so the check
119
+ // cannot go vacuous behind an export change) and that the direct-DB pair is not.
120
+ const clientOptions = declarations.match(/interface NeopressClientOptions\s*\{([\s\S]*?)\n\}/);
121
+ if (!clientOptions) {
122
+ errors.push(
123
+ `dist/index.d.ts no longer declares NeopressClientOptions\n` +
124
+ ` The W7 direct-DB guard reads that interface. If the surface legitimately\n` +
125
+ ` stopped exposing NeopressClient, re-point the guard instead of dropping it.`,
126
+ );
127
+ } else {
128
+ const reopened = ["supabaseUrl", "supabaseAnonKey"].filter((member) =>
129
+ new RegExp(`^\\s*${member}\\??:`, "m").test(clientOptions[1]),
84
130
  );
131
+ if (reopened.length > 0) {
132
+ errors.push(
133
+ `NeopressClientOptions re-declares direct-DB config: ${reopened.join(", ")}\n` +
134
+ ` The SDK must reach the API over HTTP only — assets/collections/entries/\n` +
135
+ ` entry_references/forms/pages/redirect_rules revoked their authenticated\n` +
136
+ ` write grants on that basis (docs/master/2026-09-07-billing-and-access.md).`,
137
+ );
138
+ }
85
139
  }
86
140
 
87
141
  if (errors.length > 0) {
88
- console.error(`[check-dist-contract] ${packageJson.name} failed:\n\n${errors.join('\n\n')}\n`);
142
+ console.error(`[check-dist-contract] ${packageJson.name} failed:\n\n${errors.join("\n\n")}\n`);
89
143
  process.exit(1);
90
144
  }
91
145
 
92
- console.log(`[check-dist-contract] ${packageJson.name} ok — ${actualFiles.length} files, no unresolvable type imports`);
146
+ console.log(
147
+ `[check-dist-contract] ${packageJson.name} ok — ${actualFiles.length} files, no unresolvable type imports`,
148
+ );
@@ -15,7 +15,6 @@ PUBLIC_URL="${NEOPRESS_MCP_PUBLIC_URL:-}"
15
15
  BASE_URL="${NEOPRESS_BASE_URL:-https://app.neopress.ai}"
16
16
  AUTH_MODE="${NEOPRESS_MCP_AUTH_MODE:-oauth}"
17
17
  CORS_ORIGINS="${NEOPRESS_MCP_CORS_ORIGINS:-}"
18
- OAUTH_PROVIDER="${NEOPRESS_MCP_OAUTH_PROVIDER:-google}"
19
18
 
20
19
  require_command() {
21
20
  if ! command -v "$1" >/dev/null 2>&1; then
@@ -27,6 +26,13 @@ require_command() {
27
26
  ensure_gcp_services_enabled() {
28
27
  require_command gcloud
29
28
 
29
+ # CI deployer SA has no serviceusage.serviceUsageAdmin; services are already
30
+ # enabled on this project, so CI sets SKIP_SERVICE_ENABLE=1.
31
+ if [[ "${SKIP_SERVICE_ENABLE:-0}" == "1" ]]; then
32
+ echo "Skipping GCP service enablement (SKIP_SERVICE_ENABLE=1)"
33
+ return
34
+ fi
35
+
30
36
  local services=(
31
37
  artifactregistry.googleapis.com
32
38
  cloudbuild.googleapis.com
@@ -125,17 +131,8 @@ env_vars=(
125
131
  "NODE_ENV=production"
126
132
  "NEOPRESS_BASE_URL=${BASE_URL}"
127
133
  "NEOPRESS_MCP_AUTH_MODE=${AUTH_MODE}"
128
- "NEOPRESS_MCP_OAUTH_PROVIDER=${OAUTH_PROVIDER}"
129
134
  )
130
135
 
131
- if [[ -n "${NEOPRESS_MCP_OAUTH_CLIENT_SECRET:-}" ]]; then
132
- env_vars+=("NEOPRESS_MCP_OAUTH_CLIENT_SECRET=${NEOPRESS_MCP_OAUTH_CLIENT_SECRET}")
133
- elif [[ "${service_exists}" == false ]]; then
134
- echo "NEOPRESS_MCP_OAUTH_CLIENT_SECRET is required for first production remote MCP deploy." >&2
135
- echo "Use a stable random value, for example: openssl rand -base64 32" >&2
136
- exit 1
137
- fi
138
-
139
136
  if [[ -n "${PUBLIC_URL}" ]]; then
140
137
  env_vars+=("NEOPRESS_MCP_PUBLIC_URL=${PUBLIC_URL}")
141
138
  fi
@@ -152,14 +149,36 @@ if [[ -n "${NEOPRESS_SUPABASE_ANON_KEY:-}" ]]; then
152
149
  env_vars+=("NEOPRESS_SUPABASE_ANON_KEY=${NEOPRESS_SUPABASE_ANON_KEY}")
153
150
  fi
154
151
 
155
- env_arg="^~^$(IFS=~; echo "${env_vars[*]}")"
152
+ if [[ -n "${UPSTASH_REDIS_REST_URL:-}" ]]; then
153
+ env_vars+=("UPSTASH_REDIS_REST_URL=${UPSTASH_REDIS_REST_URL}")
154
+ fi
155
+
156
+ if [[ -n "${UPSTASH_REDIS_REST_TOKEN:-}" ]]; then
157
+ env_vars+=("UPSTASH_REDIS_REST_TOKEN=${UPSTASH_REDIS_REST_TOKEN}")
158
+ fi
159
+
160
+ if [[ -n "${OPENAI_APPS_CHALLENGE_TOKEN:-}" ]]; then
161
+ env_vars+=("OPENAI_APPS_CHALLENGE_TOKEN=${OPENAI_APPS_CHALLENGE_TOKEN}")
162
+ fi
163
+
164
+ # A YAML env file instead of delimiter syntax: the "^~^k=v~k=v" form was
165
+ # observed collapsing every pair into a single NODE_ENV value on the CI runner
166
+ # (revisions 00001/00002 shipped one 276-char env var), which silently dropped
167
+ # the Upstash credentials and NEOPRESS_MCP_PUBLIC_URL. --env-vars-file has no
168
+ # delimiter to mis-parse; it replaces the full env set on every deploy, which
169
+ # is the intent here (this array is the complete contract).
170
+ env_file="$(mktemp)"
171
+ trap 'rm -f "${env_file}"' EXIT
172
+ for pair in "${env_vars[@]}"; do
173
+ printf '%s: "%s"\n' "${pair%%=*}" "${pair#*=}" >> "${env_file}"
174
+ done
156
175
 
157
176
  if [[ "${service_exists}" == true ]]; then
158
177
  gcloud run services update "${SERVICE}" \
159
178
  --project "${PROJECT_ID}" \
160
179
  --region "${REGION}" \
161
180
  --image "${IMAGE}" \
162
- --update-env-vars "${env_arg}"
181
+ --env-vars-file "${env_file}"
163
182
  else
164
183
  gcloud run deploy "${SERVICE}" \
165
184
  --project "${PROJECT_ID}" \
@@ -171,7 +190,7 @@ else
171
190
  --cpu 1 \
172
191
  --min-instances 0 \
173
192
  --max-instances 10 \
174
- --set-env-vars "${env_arg}"
193
+ --env-vars-file "${env_file}"
175
194
  fi
176
195
 
177
196
  echo "Latest ready revision:"