@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.
- package/README.md +83 -19
- package/dist/bin.cjs +664 -23934
- package/dist/http-bin.cjs +7070 -25446
- package/dist/index.d.ts +90 -70
- package/dist/index.js +682 -24152
- package/integrations/README.md +7 -7
- package/integrations/chatgpt/app-draft.md +111 -17
- package/integrations/claude-remote/connector-draft.md +53 -16
- package/integrations/submission-guide.md +61 -0
- package/package.json +5 -2
- package/scripts/check-dist-contract.mjs +81 -25
- package/scripts/deploy-cloud-run.sh +32 -13
- package/scripts/prepare-mcpb-package.mjs +29 -35
package/integrations/README.md
CHANGED
|
@@ -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
|
-
-
|
|
22
|
-
|
|
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
|
-
-
|
|
39
|
-
|
|
40
|
-
|
|
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:
|
|
4
|
-
|
|
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 |
|
|
17
|
-
| Terms |
|
|
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
|
-
|
|
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
|
-
-
|
|
42
|
-
-
|
|
43
|
-
-
|
|
44
|
-
-
|
|
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
|
-
##
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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:
|
|
4
|
-
|
|
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 |
|
|
17
|
-
| Terms URL |
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
71
|
-
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
"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/
|
|
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
|
|
7
|
-
import
|
|
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,
|
|
11
|
-
const packageJson = JSON.parse(readFileSync(path.join(packageRoot,
|
|
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 = [
|
|
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(
|
|
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),
|
|
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(
|
|
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(
|
|
57
|
-
? readFileSync(path.join(distDir,
|
|
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(
|
|
74
|
-
const packageName = specifier.startsWith(
|
|
75
|
-
? specifier.split(
|
|
76
|
-
: specifier.split(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
--
|
|
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
|
-
--
|
|
193
|
+
--env-vars-file "${env_file}"
|
|
175
194
|
fi
|
|
176
195
|
|
|
177
196
|
echo "Latest ready revision:"
|