@1llum1n4t1/cws-mcp 1.4.2
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/LICENSE +22 -0
- package/README.ko.md +216 -0
- package/README.md +217 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +722 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mikusnuz (original author)
|
|
4
|
+
Copyright (c) 2026 1llum1n4t1s (fork)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
|
7
|
+
copy of this software and associated documentation files (the "Software"),
|
|
8
|
+
to deal in the Software without restriction, including without limitation
|
|
9
|
+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
10
|
+
and/or sell copies of the Software, and to permit persons to whom the
|
|
11
|
+
Software is furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in
|
|
14
|
+
all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
22
|
+
DEALINGS IN THE SOFTWARE.
|
package/README.ko.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# @1llum1n4t1/cws-mcp
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@1llum1n4t1/cws-mcp)
|
|
4
|
+
|
|
5
|
+
[English](README.md)
|
|
6
|
+
|
|
7
|
+
Chrome Web Store 확장 프로그램 관리를 위한 MCP 서버. Claude Code 또는 MCP 클라이언트에서 직접 크롬 확장 프로그램을 업로드, 퍼블리시, 관리할 수 있습니다.
|
|
8
|
+
|
|
9
|
+
> [mikusnuz/cws-mcp](https://github.com/mikusnuz/cws-mcp)의 포크. Chrome Web Store **API V2**에 맞춰 서비스 계정 인증, 동작하는 OAuth 플로우(Google이 기존 `oob` 플로우를 제거함), 최신 MCP SDK로 갱신했습니다.
|
|
10
|
+
|
|
11
|
+
## 이런 경우에 사용하세요
|
|
12
|
+
|
|
13
|
+
- **"크롬 확장 프로그램 새 버전 업로드해줘"** — ZIP을 빌드하고 `upload` 도구로 초안 업데이트
|
|
14
|
+
- **"확장 프로그램 Chrome Web Store에 퍼블리시해줘"** — `publish`로 리뷰 제출 및 배포
|
|
15
|
+
- **"확장 프로그램 리뷰 상태 확인해줘"** — `status`로 리뷰 상태, 버전, 배포 비율 확인
|
|
16
|
+
- **"확장 프로그램 메타데이터(설명, 스크린샷) 업데이트해줘"** — `update-metadata-ui`로 스토어 리스팅 수정
|
|
17
|
+
- **"제출 대기 중인 거 취소해줘"** — `cancel`로 리뷰 중인 제출 철회
|
|
18
|
+
- **"확장 프로그램 단계적 배포 설정해줘"** — `publish`로 단계적 배포 후 `deploy-percentage`로 비율 증가
|
|
19
|
+
|
|
20
|
+
## 도구
|
|
21
|
+
|
|
22
|
+
| 도구 | 설명 |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `upload` | ZIP 파일을 Chrome Web Store에 업로드 (기존 항목 초안 업데이트) |
|
|
25
|
+
| `publish` | 단계적 배포, 퍼블리시 유형, 리뷰 건너뛰기 옵션으로 확장 프로그램 퍼블리시 |
|
|
26
|
+
| `status` | 리뷰 상태, 배포 비율, 버전 등 현재 상태 확인 |
|
|
27
|
+
| `cancel` | 제출 대기 중인 항목 취소 |
|
|
28
|
+
| `deploy-percentage` | 단계적 배포 비율 설정 (0-100, 현재 목표보다 높아야 함) |
|
|
29
|
+
| `get` | DRAFT/PUBLISHED 리스팅 메타데이터 조회 (v1.1 API, 2026년 10월 지원 종료) |
|
|
30
|
+
| `update-metadata` | v1.1 API로 리스팅 메타데이터 업데이트 (2026년 10월 지원 종료) |
|
|
31
|
+
| `update-metadata-ui` | 대시보드 UI 자동화(Playwright)로 리스팅 메타데이터 업데이트 |
|
|
32
|
+
|
|
33
|
+
## API 커버리지
|
|
34
|
+
|
|
35
|
+
이 MCP 서버는 **모든 Chrome Web Store API v2 엔드포인트**를 지원합니다:
|
|
36
|
+
|
|
37
|
+
| v2 엔드포인트 | MCP 도구 |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `media.upload` | `upload` |
|
|
40
|
+
| `publishers.items.publish` | `publish` |
|
|
41
|
+
| `publishers.items.fetchStatus` | `status` |
|
|
42
|
+
| `publishers.items.cancelSubmission` | `cancel` |
|
|
43
|
+
| `publishers.items.setPublishedDeployPercentage` | `deploy-percentage` |
|
|
44
|
+
|
|
45
|
+
추가로, 메타데이터 조작을 위한 v1.1 API 엔드포인트(`get`, `update-metadata`)가 제공되며, v1 지원 종료에 대비하여 대시보드 UI 자동화(`update-metadata-ui`)를 권장합니다.
|
|
46
|
+
|
|
47
|
+
## 설정
|
|
48
|
+
|
|
49
|
+
**서비스 계정**(권장, CI/CD에 적합) 또는 **OAuth2 refresh token** 중 하나로 인증합니다.
|
|
50
|
+
|
|
51
|
+
### 방법 A — 서비스 계정 (권장)
|
|
52
|
+
|
|
53
|
+
1. [Google Cloud Console](https://console.cloud.google.com/)에서 프로젝트를 생성/선택하고 **Chrome Web Store API**를 활성화합니다.
|
|
54
|
+
2. **서비스 계정**을 만들고 **JSON 키**를 추가합니다 (키 → 키 추가 → 새 키 만들기 → JSON).
|
|
55
|
+
3. [개발자 대시보드](https://chrome.google.com/webstore/devconsole) → **계정**에서 서비스 계정 이메일을 추가해 퍼블리셔 계정에 대한 API 접근 권한을 부여합니다.
|
|
56
|
+
4. `CWS_SERVICE_ACCOUNT_KEY`에 JSON 키 **파일 경로**(또는 JSON 원문)를 설정합니다.
|
|
57
|
+
|
|
58
|
+
자세한 내용은 [서비스 계정으로 Chrome Web Store API 사용하기](https://developer.chrome.com/docs/webstore/service-accounts)를 참고하세요.
|
|
59
|
+
|
|
60
|
+
### 방법 B — OAuth2 Refresh Token
|
|
61
|
+
|
|
62
|
+
1. [Google Cloud Console](https://console.cloud.google.com/)에서 프로젝트를 생성/선택하고 **Chrome Web Store API**를 활성화합니다.
|
|
63
|
+
2. **데스크톱 앱** 유형의 **OAuth2 자격 증명**을 생성하고 **Client ID**와 **Client Secret**을 기록합니다.
|
|
64
|
+
3. **loopback 리디렉션**으로 refresh token을 발급받습니다. (기존 `urn:ietf:wg:oauth:2.0:oob` 플로우는 2022년에 Google이 제거하여 더 이상 동작하지 않습니다 — 데스크톱 클라이언트는 이제 `http://localhost`를 사용합니다.)
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 1. 브라우저에서 아래 URL을 열고 접근을 허용합니다:
|
|
68
|
+
# https://accounts.google.com/o/oauth2/v2/auth?response_type=code&access_type=offline&prompt=consent&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fchromewebstore&client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:8080
|
|
69
|
+
#
|
|
70
|
+
# 2. 브라우저가 http://localhost:8080/?code=AUTH_CODE&... 로 리디렉션됩니다.
|
|
71
|
+
# (서버 불필요 — 주소창에서 `code` 값을 복사하세요).
|
|
72
|
+
#
|
|
73
|
+
# 3. 코드를 refresh token으로 교환:
|
|
74
|
+
curl -X POST https://oauth2.googleapis.com/token \
|
|
75
|
+
-d "client_id=YOUR_CLIENT_ID" \
|
|
76
|
+
-d "client_secret=YOUR_CLIENT_SECRET" \
|
|
77
|
+
-d "code=YOUR_AUTH_CODE" \
|
|
78
|
+
-d "grant_type=authorization_code" \
|
|
79
|
+
-d "redirect_uri=http://localhost:8080"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
응답에 `refresh_token`이 포함됩니다. 데스크톱 앱 클라이언트는 `http://localhost[:PORT]` 또는 `http://127.0.0.1[:PORT]` 리디렉션을 허용합니다.
|
|
83
|
+
|
|
84
|
+
### 3. MCP 설정
|
|
85
|
+
|
|
86
|
+
Claude Code MCP 설정 (`~/.claude/settings.local.json`)에 추가합니다.
|
|
87
|
+
|
|
88
|
+
**npm + 서비스 계정 (권장):**
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"mcpServers": {
|
|
93
|
+
"cws-mcp": {
|
|
94
|
+
"command": "npx",
|
|
95
|
+
"args": ["-y", "@1llum1n4t1/cws-mcp"],
|
|
96
|
+
"env": {
|
|
97
|
+
"CWS_SERVICE_ACCOUNT_KEY": "/path/to/service-account.json",
|
|
98
|
+
"CWS_PUBLISHER_ID": "me",
|
|
99
|
+
"CWS_ITEM_ID": "확장프로그램ID"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**로컬 클론 + OAuth refresh token:**
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"mcpServers": {
|
|
111
|
+
"cws-mcp": {
|
|
112
|
+
"command": "node",
|
|
113
|
+
"args": ["/path/to/cws-mcp/dist/index.js"],
|
|
114
|
+
"env": {
|
|
115
|
+
"CWS_CLIENT_ID": "xxxxx.apps.googleusercontent.com",
|
|
116
|
+
"CWS_CLIENT_SECRET": "GOCSPX-xxxxx",
|
|
117
|
+
"CWS_REFRESH_TOKEN": "1//xxxxx",
|
|
118
|
+
"CWS_PUBLISHER_ID": "me",
|
|
119
|
+
"CWS_ITEM_ID": "확장프로그램ID"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## 환경 변수
|
|
127
|
+
|
|
128
|
+
**서비스 계정 키(인증 A)** 또는 **OAuth2 refresh token 3종(인증 B)** 중 하나를 제공하세요.
|
|
129
|
+
|
|
130
|
+
| 변수 | 필수 | 설명 |
|
|
131
|
+
|---|---|---|
|
|
132
|
+
| `CWS_SERVICE_ACCOUNT_KEY` | 인증 A | 서비스 계정 JSON 키 파일 경로 또는 JSON 원문. 설정 시 OAuth보다 우선합니다. |
|
|
133
|
+
| `CWS_CLIENT_ID` | 인증 B | Google OAuth2 Client ID |
|
|
134
|
+
| `CWS_CLIENT_SECRET` | 인증 B | Google OAuth2 Client Secret |
|
|
135
|
+
| `CWS_REFRESH_TOKEN` | 인증 B | OAuth2 Refresh Token |
|
|
136
|
+
| `CWS_PUBLISHER_ID` | 아니오 | 퍼블리셔 ID (기본값: `me`) |
|
|
137
|
+
| `CWS_ITEM_ID` | 아니오 | 기본 확장 프로그램 Item ID |
|
|
138
|
+
| `CWS_DASHBOARD_PROFILE_DIR` | 아니오 | `update-metadata-ui`용 브라우저 프로필 경로 (기본값: `~/.cws-mcp-profile`) |
|
|
139
|
+
|
|
140
|
+
## 사용 예시
|
|
141
|
+
|
|
142
|
+
### 확장 프로그램 상태 확인
|
|
143
|
+
```
|
|
144
|
+
cws-mcp status 도구 사용
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 업로드 후 퍼블리시
|
|
148
|
+
```
|
|
149
|
+
1. cws-mcp upload (zipPath="/path/to/extension.zip")
|
|
150
|
+
2. cws-mcp publish
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 단계적 배포로 퍼블리시
|
|
154
|
+
```
|
|
155
|
+
cws-mcp publish 사용:
|
|
156
|
+
- publishType="STAGED_PUBLISH"
|
|
157
|
+
- deployPercentage=10
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 리뷰 건너뛰기로 퍼블리시
|
|
161
|
+
```
|
|
162
|
+
cws-mcp publish에서 skipReview=true 사용
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 퍼블리시 없이 제목/설명 업데이트
|
|
166
|
+
```
|
|
167
|
+
cws-mcp update-metadata 사용:
|
|
168
|
+
- title="Pexus"
|
|
169
|
+
- summary="Official wallet for Plumise"
|
|
170
|
+
- description="..."
|
|
171
|
+
- category="productivity"
|
|
172
|
+
- defaultLocale="en"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### 고급 메타데이터 업데이트
|
|
176
|
+
```
|
|
177
|
+
cws-mcp update-metadata에서 metadata 객체 전달:
|
|
178
|
+
{
|
|
179
|
+
"homepageUrl": "https://plumise.com",
|
|
180
|
+
"supportUrl": "https://plug.plumise.com/docs"
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### API 반영이 안 되는 경우(UI 자동화)
|
|
185
|
+
```
|
|
186
|
+
cws-mcp update-metadata-ui 사용:
|
|
187
|
+
- title
|
|
188
|
+
- summary
|
|
189
|
+
- description
|
|
190
|
+
- category
|
|
191
|
+
- homepageUrl
|
|
192
|
+
- supportUrl
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
참고:
|
|
196
|
+
- 이 도구는 Chrome Web Store 대시보드 UI를 자동 조작합니다.
|
|
197
|
+
- 로그인 필요 시 `headless=false`로 1회 실행해 로그인하세요.
|
|
198
|
+
- 브라우저 프로필 기본 경로: `~/.cws-mcp-profile` (`CWS_DASHBOARD_PROFILE_DIR`로 변경 가능)
|
|
199
|
+
|
|
200
|
+
### 단계적 배포
|
|
201
|
+
```
|
|
202
|
+
1. cws-mcp publish
|
|
203
|
+
2. cws-mcp deploy-percentage (percentage=10)
|
|
204
|
+
3. cws-mcp deploy-percentage (percentage=50)
|
|
205
|
+
4. cws-mcp deploy-percentage (percentage=100)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
참고: `deploy-percentage`는 7일 활성 사용자 10,000명 이상인 확장 프로그램에서만 사용 가능합니다. 새 비율은 항상 현재 목표보다 높아야 합니다.
|
|
209
|
+
|
|
210
|
+
## V1 API 지원 종료 안내
|
|
211
|
+
|
|
212
|
+
`get`과 `update-metadata` 도구는 Chrome Web Store v1.1 API를 사용하며, **2026년 10월 15일 이후 지원이 종료**됩니다. v2 API에는 메타데이터 읽기/쓰기 엔드포인트가 없어 이 도구들이 브릿지 역할을 합니다. 장기적으로는 `update-metadata-ui` (Playwright 대시보드 자동화)를 대안으로 사용하세요.
|
|
213
|
+
|
|
214
|
+
## 라이선스
|
|
215
|
+
|
|
216
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# @1llum1n4t1/cws-mcp
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@1llum1n4t1/cws-mcp)
|
|
4
|
+
|
|
5
|
+
[한국어](README.ko.md)
|
|
6
|
+
|
|
7
|
+
MCP server for Chrome Web Store extension management. Upload, publish, and manage Chrome extensions directly from Claude Code or any MCP client.
|
|
8
|
+
|
|
9
|
+
> Fork of [mikusnuz/cws-mcp](https://github.com/mikusnuz/cws-mcp), updated for the Chrome Web Store **API V2**: service-account auth, a working OAuth flow (Google removed the old `oob` flow), and the latest MCP SDK.
|
|
10
|
+
|
|
11
|
+
## When to Use
|
|
12
|
+
|
|
13
|
+
Use this MCP when you need to:
|
|
14
|
+
|
|
15
|
+
- **"Upload a new version of my Chrome extension"** — build your ZIP and use the `upload` tool to push it as a draft
|
|
16
|
+
- **"Publish my extension to the Chrome Web Store"** — use `publish` to submit for review and go live
|
|
17
|
+
- **"Check the review status of my extension"** — use `status` to see review state, version, and deploy percentage
|
|
18
|
+
- **"Update my extension's metadata (description, screenshots)"** — use `update-metadata-ui` to change store listing details
|
|
19
|
+
- **"Cancel a pending submission"** — use `cancel` to withdraw a submission under review
|
|
20
|
+
- **"Set up staged rollout for my extension"** — use `publish` with staged rollout, then `deploy-percentage` to ramp up
|
|
21
|
+
|
|
22
|
+
## Tools
|
|
23
|
+
|
|
24
|
+
| Tool | Description |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `upload` | Upload a ZIP file to Chrome Web Store (update existing item draft) |
|
|
27
|
+
| `publish` | Publish an extension with optional staged rollout, publish type, and skip-review |
|
|
28
|
+
| `status` | Fetch the current status including review state, deploy percentage, and version |
|
|
29
|
+
| `cancel` | Cancel a pending submission |
|
|
30
|
+
| `deploy-percentage` | Set staged rollout percentage (0-100, must exceed current target) |
|
|
31
|
+
| `get` | Read draft/published listing metadata (v1.1 API, deprecated Oct 2026) |
|
|
32
|
+
| `update-metadata` | Update listing metadata via v1.1 API (deprecated Oct 2026) |
|
|
33
|
+
| `update-metadata-ui` | Update listing metadata via dashboard UI automation (Playwright) |
|
|
34
|
+
|
|
35
|
+
## API Coverage
|
|
36
|
+
|
|
37
|
+
This MCP server covers **all Chrome Web Store API v2 endpoints**:
|
|
38
|
+
|
|
39
|
+
| v2 Endpoint | MCP Tool |
|
|
40
|
+
|---|---|
|
|
41
|
+
| `media.upload` | `upload` |
|
|
42
|
+
| `publishers.items.publish` | `publish` |
|
|
43
|
+
| `publishers.items.fetchStatus` | `status` |
|
|
44
|
+
| `publishers.items.cancelSubmission` | `cancel` |
|
|
45
|
+
| `publishers.items.setPublishedDeployPercentage` | `deploy-percentage` |
|
|
46
|
+
|
|
47
|
+
Additionally, v1.1 API endpoints are available for metadata operations (`get`, `update-metadata`), with dashboard UI automation (`update-metadata-ui`) as the recommended alternative since v1 is deprecated.
|
|
48
|
+
|
|
49
|
+
## Setup
|
|
50
|
+
|
|
51
|
+
Authenticate with **either** a service account (recommended, ideal for CI/CD) **or** an OAuth2 refresh token.
|
|
52
|
+
|
|
53
|
+
### Option A — Service Account (recommended)
|
|
54
|
+
|
|
55
|
+
1. In the [Google Cloud Console](https://console.cloud.google.com/), create/select a project and enable the **Chrome Web Store API**.
|
|
56
|
+
2. Create a **Service Account**, then add a **JSON key** (Keys → Add key → Create new key → JSON).
|
|
57
|
+
3. In the [Developer Dashboard](https://chrome.google.com/webstore/devconsole) → **Account**, add the service account's email to grant it API access to your publisher account.
|
|
58
|
+
4. Set `CWS_SERVICE_ACCOUNT_KEY` to the **path of the JSON key file** (or its raw JSON contents).
|
|
59
|
+
|
|
60
|
+
See [Use a service account with the Chrome Web Store API](https://developer.chrome.com/docs/webstore/service-accounts) for details.
|
|
61
|
+
|
|
62
|
+
### Option B — OAuth2 Refresh Token
|
|
63
|
+
|
|
64
|
+
1. In the [Google Cloud Console](https://console.cloud.google.com/), create/select a project and enable the **Chrome Web Store API**.
|
|
65
|
+
2. Create **OAuth2 credentials** of type **Desktop app**. Note the **Client ID** and **Client Secret**.
|
|
66
|
+
3. Obtain a refresh token using a **loopback redirect**. (The old `urn:ietf:wg:oauth:2.0:oob` flow was removed by Google in 2022 and no longer works — desktop clients now use `http://localhost`.)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# 1. Open this URL in a browser and grant access:
|
|
70
|
+
# https://accounts.google.com/o/oauth2/v2/auth?response_type=code&access_type=offline&prompt=consent&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fchromewebstore&client_id=YOUR_CLIENT_ID&redirect_uri=http://localhost:8080
|
|
71
|
+
#
|
|
72
|
+
# 2. The browser is redirected to http://localhost:8080/?code=AUTH_CODE&...
|
|
73
|
+
# (no server needed — just copy the `code` value from the address bar).
|
|
74
|
+
#
|
|
75
|
+
# 3. Exchange the code for a refresh token:
|
|
76
|
+
curl -X POST https://oauth2.googleapis.com/token \
|
|
77
|
+
-d "client_id=YOUR_CLIENT_ID" \
|
|
78
|
+
-d "client_secret=YOUR_CLIENT_SECRET" \
|
|
79
|
+
-d "code=YOUR_AUTH_CODE" \
|
|
80
|
+
-d "grant_type=authorization_code" \
|
|
81
|
+
-d "redirect_uri=http://localhost:8080"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The response contains your `refresh_token`. Any `http://localhost[:PORT]` or `http://127.0.0.1[:PORT]` redirect is accepted for Desktop-app clients.
|
|
85
|
+
|
|
86
|
+
### 3. Configure MCP
|
|
87
|
+
|
|
88
|
+
Add to your Claude Code MCP settings (`~/.claude/settings.local.json`).
|
|
89
|
+
|
|
90
|
+
**Via npm with a service account (recommended):**
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"mcpServers": {
|
|
95
|
+
"cws-mcp": {
|
|
96
|
+
"command": "npx",
|
|
97
|
+
"args": ["-y", "@1llum1n4t1/cws-mcp"],
|
|
98
|
+
"env": {
|
|
99
|
+
"CWS_SERVICE_ACCOUNT_KEY": "/path/to/service-account.json",
|
|
100
|
+
"CWS_PUBLISHER_ID": "me",
|
|
101
|
+
"CWS_ITEM_ID": "your-extension-id"
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**From a local clone with an OAuth refresh token:**
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"mcpServers": {
|
|
113
|
+
"cws-mcp": {
|
|
114
|
+
"command": "node",
|
|
115
|
+
"args": ["/path/to/cws-mcp/dist/index.js"],
|
|
116
|
+
"env": {
|
|
117
|
+
"CWS_CLIENT_ID": "xxxxx.apps.googleusercontent.com",
|
|
118
|
+
"CWS_CLIENT_SECRET": "GOCSPX-xxxxx",
|
|
119
|
+
"CWS_REFRESH_TOKEN": "1//xxxxx",
|
|
120
|
+
"CWS_PUBLISHER_ID": "me",
|
|
121
|
+
"CWS_ITEM_ID": "your-extension-id"
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Environment Variables
|
|
129
|
+
|
|
130
|
+
Provide **either** a service-account key (Auth A) **or** the OAuth2 refresh-token trio (Auth B).
|
|
131
|
+
|
|
132
|
+
| Variable | Required | Description |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `CWS_SERVICE_ACCOUNT_KEY` | Auth A | Path to a service-account JSON key file, or the raw JSON string. Takes precedence over OAuth when set. |
|
|
135
|
+
| `CWS_CLIENT_ID` | Auth B | Google OAuth2 Client ID |
|
|
136
|
+
| `CWS_CLIENT_SECRET` | Auth B | Google OAuth2 Client Secret |
|
|
137
|
+
| `CWS_REFRESH_TOKEN` | Auth B | OAuth2 Refresh Token |
|
|
138
|
+
| `CWS_PUBLISHER_ID` | No | Publisher ID (default: `me`) |
|
|
139
|
+
| `CWS_ITEM_ID` | No | Default extension item ID |
|
|
140
|
+
| `CWS_DASHBOARD_PROFILE_DIR` | No | Browser profile path for `update-metadata-ui` (default: `~/.cws-mcp-profile`) |
|
|
141
|
+
|
|
142
|
+
## Usage Examples
|
|
143
|
+
|
|
144
|
+
### Check extension status
|
|
145
|
+
```
|
|
146
|
+
Use the cws-mcp status tool
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Upload and publish
|
|
150
|
+
```
|
|
151
|
+
1. Use cws-mcp upload with zipPath="/path/to/extension.zip"
|
|
152
|
+
2. Use cws-mcp publish
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Publish with staged rollout
|
|
156
|
+
```
|
|
157
|
+
Use cws-mcp publish with:
|
|
158
|
+
- publishType="STAGED_PUBLISH"
|
|
159
|
+
- deployPercentage=10
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Publish with skip-review
|
|
163
|
+
```
|
|
164
|
+
Use cws-mcp publish with skipReview=true
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Update listing title/description without publishing
|
|
168
|
+
```
|
|
169
|
+
Use cws-mcp update-metadata with:
|
|
170
|
+
- title="Pexus"
|
|
171
|
+
- summary="Official wallet for Plumise"
|
|
172
|
+
- description="..."
|
|
173
|
+
- category="productivity"
|
|
174
|
+
- defaultLocale="en"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Update advanced metadata fields
|
|
178
|
+
```
|
|
179
|
+
Use cws-mcp update-metadata with metadata={
|
|
180
|
+
"homepageUrl": "https://plumise.com",
|
|
181
|
+
"supportUrl": "https://plug.plumise.com/docs"
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### When API metadata updates don't reflect
|
|
186
|
+
```
|
|
187
|
+
Use cws-mcp update-metadata-ui with:
|
|
188
|
+
- title
|
|
189
|
+
- summary
|
|
190
|
+
- description
|
|
191
|
+
- category
|
|
192
|
+
- homepageUrl
|
|
193
|
+
- supportUrl
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Notes:
|
|
197
|
+
- This tool automates the Chrome Web Store dashboard UI.
|
|
198
|
+
- First run with `headless=false` if login is required.
|
|
199
|
+
- Browser profile path defaults to `~/.cws-mcp-profile` (override with `CWS_DASHBOARD_PROFILE_DIR`).
|
|
200
|
+
|
|
201
|
+
### Staged rollout
|
|
202
|
+
```
|
|
203
|
+
1. Use cws-mcp publish
|
|
204
|
+
2. Use cws-mcp deploy-percentage with percentage=10
|
|
205
|
+
3. Use cws-mcp deploy-percentage with percentage=50
|
|
206
|
+
4. Use cws-mcp deploy-percentage with percentage=100
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Note: `deploy-percentage` is only available for extensions with 10,000+ seven-day active users. The new percentage must always be higher than the current target.
|
|
210
|
+
|
|
211
|
+
## V1 API Deprecation
|
|
212
|
+
|
|
213
|
+
The `get` and `update-metadata` tools use the Chrome Web Store v1.1 API, which is **deprecated and will be removed after October 15, 2026**. The v2 API does not provide metadata read/write endpoints, so these tools remain available as a bridge. Use `update-metadata-ui` (Playwright dashboard automation) as the long-term alternative.
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { createSign } from "node:crypto";
|
|
7
|
+
import { chromium } from "playwright";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import { resolve, join, dirname } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
// ── Version ──
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
15
|
+
const VERSION = pkg.version;
|
|
16
|
+
// ── Config ──
|
|
17
|
+
const CLIENT_ID = process.env.CWS_CLIENT_ID || "";
|
|
18
|
+
const CLIENT_SECRET = process.env.CWS_CLIENT_SECRET || "";
|
|
19
|
+
const REFRESH_TOKEN = process.env.CWS_REFRESH_TOKEN || "";
|
|
20
|
+
const SERVICE_ACCOUNT_KEY = process.env.CWS_SERVICE_ACCOUNT_KEY || "";
|
|
21
|
+
const PUBLISHER_ID = process.env.CWS_PUBLISHER_ID || "me";
|
|
22
|
+
const DEFAULT_ITEM_ID = process.env.CWS_ITEM_ID || "";
|
|
23
|
+
const API_BASE = "https://chromewebstore.googleapis.com";
|
|
24
|
+
const UPLOAD_BASE = "https://chromewebstore.googleapis.com/upload/v2";
|
|
25
|
+
const V1_BASE = "https://www.googleapis.com/chromewebstore/v1.1";
|
|
26
|
+
const TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
27
|
+
const SCOPE = "https://www.googleapis.com/auth/chromewebstore";
|
|
28
|
+
/** Date after which Google removes the Chrome Web Store v1.1 API. */
|
|
29
|
+
const V1_SUNSET = "2026-10-15";
|
|
30
|
+
const DASHBOARD_PROFILE_DIR = process.env.CWS_DASHBOARD_PROFILE_DIR || resolve(homedir(), ".cws-mcp-profile");
|
|
31
|
+
// ── Auth: OAuth2 refresh token & service account (JWT bearer) ──
|
|
32
|
+
let cachedToken = null;
|
|
33
|
+
/** Load a service account key from CWS_SERVICE_ACCOUNT_KEY (raw JSON or a file path). Returns null when not configured. */
|
|
34
|
+
function loadServiceAccount() {
|
|
35
|
+
const value = SERVICE_ACCOUNT_KEY.trim();
|
|
36
|
+
if (!value)
|
|
37
|
+
return null;
|
|
38
|
+
let raw = value;
|
|
39
|
+
if (!raw.startsWith("{")) {
|
|
40
|
+
// Treat the value as a path to a JSON key file.
|
|
41
|
+
raw = readFileSync(resolve(raw), "utf-8");
|
|
42
|
+
}
|
|
43
|
+
let key;
|
|
44
|
+
try {
|
|
45
|
+
key = JSON.parse(raw);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
throw new Error("CWS_SERVICE_ACCOUNT_KEY is neither valid JSON nor a readable JSON key file path.");
|
|
49
|
+
}
|
|
50
|
+
if (!key.client_email || !key.private_key) {
|
|
51
|
+
throw new Error("Invalid service account key: missing 'client_email' or 'private_key'.");
|
|
52
|
+
}
|
|
53
|
+
return { client_email: key.client_email, private_key: key.private_key };
|
|
54
|
+
}
|
|
55
|
+
function base64url(input) {
|
|
56
|
+
return Buffer.from(input).toString("base64url");
|
|
57
|
+
}
|
|
58
|
+
/** Mint an access token from a service account using the RS256 JWT-bearer grant. */
|
|
59
|
+
async function fetchTokenViaServiceAccount(sa) {
|
|
60
|
+
const now = Math.floor(Date.now() / 1000);
|
|
61
|
+
const header = { alg: "RS256", typ: "JWT" };
|
|
62
|
+
const claims = {
|
|
63
|
+
iss: sa.client_email,
|
|
64
|
+
scope: SCOPE,
|
|
65
|
+
aud: TOKEN_URL,
|
|
66
|
+
iat: now,
|
|
67
|
+
exp: now + 3600,
|
|
68
|
+
};
|
|
69
|
+
const signingInput = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(claims))}`;
|
|
70
|
+
const signature = createSign("RSA-SHA256").update(signingInput).sign(sa.private_key, "base64url");
|
|
71
|
+
const assertion = `${signingInput}.${signature}`;
|
|
72
|
+
const body = new URLSearchParams({
|
|
73
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
74
|
+
assertion,
|
|
75
|
+
});
|
|
76
|
+
const res = await fetch(TOKEN_URL, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
79
|
+
body: body.toString(),
|
|
80
|
+
});
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
throw new Error(`Service account token request failed (${res.status}): ${await res.text()}`);
|
|
83
|
+
}
|
|
84
|
+
return (await res.json());
|
|
85
|
+
}
|
|
86
|
+
/** Mint an access token from an OAuth2 refresh token. */
|
|
87
|
+
async function fetchTokenViaRefreshToken() {
|
|
88
|
+
const body = new URLSearchParams({
|
|
89
|
+
client_id: CLIENT_ID,
|
|
90
|
+
client_secret: CLIENT_SECRET,
|
|
91
|
+
refresh_token: REFRESH_TOKEN,
|
|
92
|
+
grant_type: "refresh_token",
|
|
93
|
+
});
|
|
94
|
+
const res = await fetch(TOKEN_URL, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
97
|
+
body: body.toString(),
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
throw new Error(`Token refresh failed (${res.status}): ${await res.text()}`);
|
|
101
|
+
}
|
|
102
|
+
return (await res.json());
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolve an access token, preferring a service account when configured and
|
|
106
|
+
* falling back to the OAuth2 refresh-token flow. Tokens are cached until expiry.
|
|
107
|
+
*/
|
|
108
|
+
async function getAccessToken() {
|
|
109
|
+
if (cachedToken && Date.now() < cachedToken.expires_at - 60_000) {
|
|
110
|
+
return cachedToken.access_token;
|
|
111
|
+
}
|
|
112
|
+
const sa = loadServiceAccount();
|
|
113
|
+
let data;
|
|
114
|
+
if (sa) {
|
|
115
|
+
data = await fetchTokenViaServiceAccount(sa);
|
|
116
|
+
}
|
|
117
|
+
else if (CLIENT_ID && CLIENT_SECRET && REFRESH_TOKEN) {
|
|
118
|
+
data = await fetchTokenViaRefreshToken();
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
throw new Error("Missing credentials. Set CWS_SERVICE_ACCOUNT_KEY (service account), " +
|
|
122
|
+
"or CWS_CLIENT_ID + CWS_CLIENT_SECRET + CWS_REFRESH_TOKEN (OAuth refresh token).");
|
|
123
|
+
}
|
|
124
|
+
cachedToken = {
|
|
125
|
+
access_token: data.access_token,
|
|
126
|
+
expires_at: Date.now() + data.expires_in * 1000,
|
|
127
|
+
};
|
|
128
|
+
return cachedToken.access_token;
|
|
129
|
+
}
|
|
130
|
+
// ── Helpers ──
|
|
131
|
+
function errMsg(e) {
|
|
132
|
+
return e instanceof Error ? e.message : String(e);
|
|
133
|
+
}
|
|
134
|
+
function resolveItemId(itemId) {
|
|
135
|
+
const id = itemId || DEFAULT_ITEM_ID;
|
|
136
|
+
if (!id) {
|
|
137
|
+
throw new Error("No item ID provided. Pass itemId parameter or set CWS_ITEM_ID env var.");
|
|
138
|
+
}
|
|
139
|
+
return id;
|
|
140
|
+
}
|
|
141
|
+
function resolvePublisherId(publisherId) {
|
|
142
|
+
return publisherId || PUBLISHER_ID;
|
|
143
|
+
}
|
|
144
|
+
async function apiCall(url, options) {
|
|
145
|
+
const token = await getAccessToken();
|
|
146
|
+
const headers = {
|
|
147
|
+
Authorization: `Bearer ${token}`,
|
|
148
|
+
...(options.headers || {}),
|
|
149
|
+
};
|
|
150
|
+
const res = await fetch(url, { ...options, headers });
|
|
151
|
+
const body = await res.text();
|
|
152
|
+
return { ok: res.ok, status: res.status, body };
|
|
153
|
+
}
|
|
154
|
+
/** Format an API response with structured error info when applicable. */
|
|
155
|
+
function formatResponse(result) {
|
|
156
|
+
if (result.ok) {
|
|
157
|
+
return { content: [{ type: "text", text: result.body }], isError: false };
|
|
158
|
+
}
|
|
159
|
+
// Try to parse the error body for a more readable message.
|
|
160
|
+
let errorDetail = result.body;
|
|
161
|
+
try {
|
|
162
|
+
const parsed = JSON.parse(result.body);
|
|
163
|
+
if (parsed.error?.message) {
|
|
164
|
+
errorDetail = `${parsed.error.message} (code: ${parsed.error.code || result.status})`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Keep raw body
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
content: [{ type: "text", text: `API Error (${result.status}): ${errorDetail}` }],
|
|
172
|
+
isError: true,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** Append an extra note (e.g. deprecation warning) to a tool result. */
|
|
176
|
+
function appendNote(result, note) {
|
|
177
|
+
return { ...result, content: [...result.content, { type: "text", text: note }] };
|
|
178
|
+
}
|
|
179
|
+
const V1_NOTE = `⚠️ This tool uses the Chrome Web Store v1.1 API, which Google will remove after ${V1_SUNSET}. ` +
|
|
180
|
+
`The v2 API has no metadata read/write endpoint — for store-listing changes, prefer the 'update-metadata-ui' tool.`;
|
|
181
|
+
function toolError(e) {
|
|
182
|
+
return { content: [{ type: "text", text: `Error: ${errMsg(e)}` }], isError: true };
|
|
183
|
+
}
|
|
184
|
+
function escapeRegExp(value) {
|
|
185
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
186
|
+
}
|
|
187
|
+
async function fillTextFieldByLabel(page, labels, value) {
|
|
188
|
+
const parts = labels.map(escapeRegExp).join("|");
|
|
189
|
+
const regex = new RegExp(parts, "i");
|
|
190
|
+
const candidates = [
|
|
191
|
+
page.getByLabel(regex).first(),
|
|
192
|
+
page.getByRole("textbox", { name: regex }).first(),
|
|
193
|
+
page.getByPlaceholder(regex).first(),
|
|
194
|
+
];
|
|
195
|
+
for (const locator of candidates) {
|
|
196
|
+
if ((await locator.count()) > 0) {
|
|
197
|
+
await locator.fill(value);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const labelNode = page.getByText(regex).first();
|
|
202
|
+
if ((await labelNode.count()) > 0) {
|
|
203
|
+
const container = labelNode.locator("xpath=ancestor::*[self::div or self::section][1]");
|
|
204
|
+
const field = container.locator("textarea, input[type='text'], input:not([type])").first();
|
|
205
|
+
if ((await field.count()) > 0) {
|
|
206
|
+
await field.fill(value);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
throw new Error(`Unable to locate field by labels: ${labels.join(", ")}`);
|
|
211
|
+
}
|
|
212
|
+
async function uploadFileBySectionLabel(page, labels, filePath) {
|
|
213
|
+
const resolvedPath = resolve(filePath);
|
|
214
|
+
const parts = labels.map(escapeRegExp).join("|");
|
|
215
|
+
const regex = new RegExp(parts, "i");
|
|
216
|
+
const labelNode = page.getByText(regex).first();
|
|
217
|
+
if ((await labelNode.count()) > 0) {
|
|
218
|
+
const container = labelNode.locator("xpath=ancestor::*[self::div or self::section][1]");
|
|
219
|
+
const fileInput = container.locator("input[type='file']").first();
|
|
220
|
+
if ((await fileInput.count()) > 0) {
|
|
221
|
+
await fileInput.setInputFiles(resolvedPath);
|
|
222
|
+
await page.waitForTimeout(1200);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const anyFileInput = page.locator("input[type='file']").first();
|
|
227
|
+
if ((await anyFileInput.count()) > 0) {
|
|
228
|
+
await anyFileInput.setInputFiles(resolvedPath);
|
|
229
|
+
await page.waitForTimeout(1200);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
throw new Error(`Unable to locate file input for labels: ${labels.join(", ")}`);
|
|
233
|
+
}
|
|
234
|
+
async function clickSaveButton(page) {
|
|
235
|
+
const roleCandidates = [
|
|
236
|
+
page.getByRole("button", { name: /save|저장|임시저장|save draft/i }).first(),
|
|
237
|
+
page.getByRole("button", { name: /submit for review|검토/i }).first(),
|
|
238
|
+
];
|
|
239
|
+
for (const saveBtn of roleCandidates) {
|
|
240
|
+
if ((await saveBtn.count()) > 0) {
|
|
241
|
+
await saveBtn.click();
|
|
242
|
+
await page.waitForTimeout(2000);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const textCandidates = [
|
|
247
|
+
page.locator("button:has-text('저장')").first(),
|
|
248
|
+
page.locator("button:has-text('임시저장')").first(),
|
|
249
|
+
page.locator("button:has-text('Save')").first(),
|
|
250
|
+
];
|
|
251
|
+
for (const saveBtn of textCandidates) {
|
|
252
|
+
if ((await saveBtn.count()) > 0) {
|
|
253
|
+
await saveBtn.click();
|
|
254
|
+
await page.waitForTimeout(2000);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if ((await page.getByText(/항목이 저장되었습니다|saved/i).count()) > 0) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if ((await page.getByText(/변경사항이 저장되지 않았|unsaved/i).count()) === 0) {
|
|
262
|
+
throw new Error("Save button not found on dashboard page.");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// ── Shared tool schemas (single source of truth for main server + sandbox) ──
|
|
266
|
+
const itemIdSchema = z
|
|
267
|
+
.string()
|
|
268
|
+
.optional()
|
|
269
|
+
.describe("Extension item ID (defaults to CWS_ITEM_ID env var)");
|
|
270
|
+
const publisherIdSchema = z
|
|
271
|
+
.string()
|
|
272
|
+
.optional()
|
|
273
|
+
.describe("Publisher ID (defaults to CWS_PUBLISHER_ID env var or 'me')");
|
|
274
|
+
const schemas = {
|
|
275
|
+
upload: {
|
|
276
|
+
zipPath: z.string().describe("Absolute path to the ZIP file to upload"),
|
|
277
|
+
itemId: itemIdSchema,
|
|
278
|
+
publisherId: publisherIdSchema,
|
|
279
|
+
},
|
|
280
|
+
publish: {
|
|
281
|
+
itemId: itemIdSchema,
|
|
282
|
+
publisherId: publisherIdSchema,
|
|
283
|
+
publishType: z
|
|
284
|
+
.enum(["DEFAULT_PUBLISH", "STAGED_PUBLISH"])
|
|
285
|
+
.optional()
|
|
286
|
+
.describe("DEFAULT_PUBLISH: publishes immediately after approval. STAGED_PUBLISH: stages for manual publishing after approval. Defaults to DEFAULT_PUBLISH."),
|
|
287
|
+
deployPercentage: z
|
|
288
|
+
.number()
|
|
289
|
+
.int()
|
|
290
|
+
.min(0)
|
|
291
|
+
.max(100)
|
|
292
|
+
.optional()
|
|
293
|
+
.describe("Initial deploy percentage for staged rollout (0-100)."),
|
|
294
|
+
skipReview: z
|
|
295
|
+
.boolean()
|
|
296
|
+
.optional()
|
|
297
|
+
.describe("Attempt to skip review if the extension qualifies. Defaults to false."),
|
|
298
|
+
blockOnWarnings: z
|
|
299
|
+
.boolean()
|
|
300
|
+
.optional()
|
|
301
|
+
.describe("If true, the publish is blocked when the submission has warnings. Defaults to false."),
|
|
302
|
+
},
|
|
303
|
+
status: {
|
|
304
|
+
itemId: itemIdSchema,
|
|
305
|
+
publisherId: publisherIdSchema,
|
|
306
|
+
},
|
|
307
|
+
cancel: {
|
|
308
|
+
itemId: itemIdSchema,
|
|
309
|
+
publisherId: publisherIdSchema,
|
|
310
|
+
},
|
|
311
|
+
"deploy-percentage": {
|
|
312
|
+
percentage: z
|
|
313
|
+
.number()
|
|
314
|
+
.min(0)
|
|
315
|
+
.max(100)
|
|
316
|
+
.describe("Deploy percentage (0-100). Must be larger than the current target percentage."),
|
|
317
|
+
itemId: itemIdSchema,
|
|
318
|
+
publisherId: publisherIdSchema,
|
|
319
|
+
},
|
|
320
|
+
get: {
|
|
321
|
+
itemId: itemIdSchema,
|
|
322
|
+
projection: z
|
|
323
|
+
.enum(["DRAFT", "PUBLISHED"])
|
|
324
|
+
.optional()
|
|
325
|
+
.describe("Metadata projection to fetch (defaults to DRAFT)"),
|
|
326
|
+
},
|
|
327
|
+
"update-metadata": {
|
|
328
|
+
itemId: itemIdSchema,
|
|
329
|
+
title: z.string().optional().describe("Store listing title"),
|
|
330
|
+
summary: z.string().optional().describe("Store listing short summary"),
|
|
331
|
+
description: z.string().optional().describe("Store listing description"),
|
|
332
|
+
category: z.string().optional().describe("Category (e.g. 'productivity', 'developer_tools')"),
|
|
333
|
+
defaultLocale: z.string().optional().describe("Default locale (e.g. 'ko', 'en')"),
|
|
334
|
+
homepageUrl: z.string().optional().describe("Homepage URL"),
|
|
335
|
+
supportUrl: z.string().optional().describe("Support URL"),
|
|
336
|
+
metadata: z
|
|
337
|
+
.record(z.unknown())
|
|
338
|
+
.optional()
|
|
339
|
+
.describe("Raw metadata object forwarded as-is to the v1.1 API. Useful for fields not exposed as first-class params."),
|
|
340
|
+
},
|
|
341
|
+
"update-metadata-ui": {
|
|
342
|
+
itemId: itemIdSchema,
|
|
343
|
+
title: z.string().optional().describe("Store listing title"),
|
|
344
|
+
summary: z.string().optional().describe("Store listing short summary"),
|
|
345
|
+
description: z.string().optional().describe("Store listing long description"),
|
|
346
|
+
category: z.string().optional().describe("Category label as shown in dashboard UI"),
|
|
347
|
+
homepageUrl: z.string().optional().describe("Homepage URL"),
|
|
348
|
+
supportUrl: z.string().optional().describe("Support URL"),
|
|
349
|
+
storeIconPath: z.string().optional().describe("Absolute path to 128x128 store icon image"),
|
|
350
|
+
accountIndex: z
|
|
351
|
+
.number()
|
|
352
|
+
.int()
|
|
353
|
+
.min(0)
|
|
354
|
+
.max(9)
|
|
355
|
+
.optional()
|
|
356
|
+
.describe("Google account index in dashboard URL (default: 0)"),
|
|
357
|
+
headless: z.boolean().optional().describe("Run browser headless (default: false)"),
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
const descriptions = {
|
|
361
|
+
upload: "Upload a ZIP file to update an existing Chrome Web Store item draft. Note: Creating new items via API is not supported in v2 — use the Developer Dashboard to create new items.",
|
|
362
|
+
publish: "Publish an extension to Chrome Web Store. Supports immediate publish, staged publish, initial deploy percentage, block-on-warnings, and skip-review.",
|
|
363
|
+
status: "Fetch the current status of an extension on Chrome Web Store. Returns published/submitted revision status, deploy percentage, version, takedown/warning flags, and last upload state.",
|
|
364
|
+
cancel: "Cancel a pending submission on Chrome Web Store. Can be used to cancel an item currently in review.",
|
|
365
|
+
"deploy-percentage": "Set the published deploy percentage for staged rollout on Chrome Web Store. The new percentage must be higher than the current target. Only available for items with 10,000+ seven-day active users.",
|
|
366
|
+
get: `Get the current metadata of a Chrome Web Store item (v1.1 API). Returns title, description, category, and other listing fields. Note: the v1.1 API is deprecated and will be removed after ${V1_SUNSET}.`,
|
|
367
|
+
"update-metadata": `Update the store listing metadata of a Chrome Web Store item (v1.1 API). Supports common fields and a raw metadata payload. Note: the v1.1 API is deprecated and will be removed after ${V1_SUNSET}. Use 'update-metadata-ui' as the long-term alternative.`,
|
|
368
|
+
"update-metadata-ui": "Update listing metadata via Chrome Web Store dashboard UI automation (Playwright). Use this when API metadata updates are not reflected, or as the primary metadata update method since the v1.1 API is deprecated.",
|
|
369
|
+
};
|
|
370
|
+
// ── MCP Server ──
|
|
371
|
+
const server = new McpServer({
|
|
372
|
+
name: "cws-mcp",
|
|
373
|
+
version: VERSION,
|
|
374
|
+
});
|
|
375
|
+
// ── upload ──
|
|
376
|
+
server.registerTool("upload", { description: descriptions.upload, inputSchema: schemas.upload }, async ({ zipPath, itemId, publisherId }) => {
|
|
377
|
+
try {
|
|
378
|
+
const id = resolveItemId(itemId);
|
|
379
|
+
const pub = resolvePublisherId(publisherId);
|
|
380
|
+
const zipData = readFileSync(zipPath);
|
|
381
|
+
const url = `${UPLOAD_BASE}/publishers/${pub}/items/${id}:upload`;
|
|
382
|
+
const result = await apiCall(url, {
|
|
383
|
+
method: "POST",
|
|
384
|
+
headers: { "Content-Type": "application/zip" },
|
|
385
|
+
body: new Uint8Array(zipData),
|
|
386
|
+
});
|
|
387
|
+
return formatResponse(result);
|
|
388
|
+
}
|
|
389
|
+
catch (e) {
|
|
390
|
+
return toolError(e);
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
// ── publish ──
|
|
394
|
+
server.registerTool("publish", { description: descriptions.publish, inputSchema: schemas.publish }, async ({ itemId, publisherId, publishType, deployPercentage, skipReview, blockOnWarnings }) => {
|
|
395
|
+
try {
|
|
396
|
+
const id = resolveItemId(itemId);
|
|
397
|
+
const pub = resolvePublisherId(publisherId);
|
|
398
|
+
const url = `${API_BASE}/v2/publishers/${pub}/items/${id}:publish`;
|
|
399
|
+
const body = {};
|
|
400
|
+
if (publishType)
|
|
401
|
+
body.publishType = publishType;
|
|
402
|
+
if (deployPercentage !== undefined)
|
|
403
|
+
body.deployInfos = [{ deployPercentage }];
|
|
404
|
+
if (skipReview !== undefined)
|
|
405
|
+
body.skipReview = skipReview;
|
|
406
|
+
if (blockOnWarnings !== undefined)
|
|
407
|
+
body.blockOnWarnings = blockOnWarnings;
|
|
408
|
+
const hasBody = Object.keys(body).length > 0;
|
|
409
|
+
const result = await apiCall(url, {
|
|
410
|
+
method: "POST",
|
|
411
|
+
...(hasBody
|
|
412
|
+
? { headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }
|
|
413
|
+
: {}),
|
|
414
|
+
});
|
|
415
|
+
return formatResponse(result);
|
|
416
|
+
}
|
|
417
|
+
catch (e) {
|
|
418
|
+
return toolError(e);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
// ── status ──
|
|
422
|
+
server.registerTool("status", { description: descriptions.status, inputSchema: schemas.status }, async ({ itemId, publisherId }) => {
|
|
423
|
+
try {
|
|
424
|
+
const id = resolveItemId(itemId);
|
|
425
|
+
const pub = resolvePublisherId(publisherId);
|
|
426
|
+
const url = `${API_BASE}/v2/publishers/${pub}/items/${id}:fetchStatus`;
|
|
427
|
+
const result = await apiCall(url, { method: "GET" });
|
|
428
|
+
return formatResponse(result);
|
|
429
|
+
}
|
|
430
|
+
catch (e) {
|
|
431
|
+
return toolError(e);
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
// ── cancel ──
|
|
435
|
+
server.registerTool("cancel", { description: descriptions.cancel, inputSchema: schemas.cancel }, async ({ itemId, publisherId }) => {
|
|
436
|
+
try {
|
|
437
|
+
const id = resolveItemId(itemId);
|
|
438
|
+
const pub = resolvePublisherId(publisherId);
|
|
439
|
+
const url = `${API_BASE}/v2/publishers/${pub}/items/${id}:cancelSubmission`;
|
|
440
|
+
const result = await apiCall(url, { method: "POST" });
|
|
441
|
+
return formatResponse(result);
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
return toolError(e);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
// ── deploy-percentage ──
|
|
448
|
+
server.registerTool("deploy-percentage", { description: descriptions["deploy-percentage"], inputSchema: schemas["deploy-percentage"] }, async ({ percentage, itemId, publisherId }) => {
|
|
449
|
+
try {
|
|
450
|
+
const id = resolveItemId(itemId);
|
|
451
|
+
const pub = resolvePublisherId(publisherId);
|
|
452
|
+
const url = `${API_BASE}/v2/publishers/${pub}/items/${id}:setPublishedDeployPercentage`;
|
|
453
|
+
const result = await apiCall(url, {
|
|
454
|
+
method: "POST",
|
|
455
|
+
headers: { "Content-Type": "application/json" },
|
|
456
|
+
body: JSON.stringify({ deployPercentage: percentage }),
|
|
457
|
+
});
|
|
458
|
+
return formatResponse(result);
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
return toolError(e);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
// ── get (v1.1 — deprecated, sunset Oct 2026) ──
|
|
465
|
+
server.registerTool("get", { description: descriptions.get, inputSchema: schemas.get }, async ({ itemId, projection }) => {
|
|
466
|
+
try {
|
|
467
|
+
const id = resolveItemId(itemId);
|
|
468
|
+
const p = projection || "DRAFT";
|
|
469
|
+
const url = `${V1_BASE}/items/${id}?projection=${encodeURIComponent(p)}`;
|
|
470
|
+
const result = await apiCall(url, { method: "GET" });
|
|
471
|
+
return appendNote(formatResponse(result), V1_NOTE);
|
|
472
|
+
}
|
|
473
|
+
catch (e) {
|
|
474
|
+
return toolError(e);
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
// ── update-metadata (v1.1 — deprecated, sunset Oct 2026) ──
|
|
478
|
+
server.registerTool("update-metadata", { description: descriptions["update-metadata"], inputSchema: schemas["update-metadata"] }, async ({ itemId, title, summary, description, category, defaultLocale, homepageUrl, supportUrl, metadata }) => {
|
|
479
|
+
try {
|
|
480
|
+
const id = resolveItemId(itemId);
|
|
481
|
+
const url = `${V1_BASE}/items/${id}`;
|
|
482
|
+
const payload = { ...(metadata || {}) };
|
|
483
|
+
if (title !== undefined)
|
|
484
|
+
payload.title = title;
|
|
485
|
+
if (summary !== undefined)
|
|
486
|
+
payload.summary = summary;
|
|
487
|
+
if (description !== undefined)
|
|
488
|
+
payload.description = description;
|
|
489
|
+
if (category !== undefined)
|
|
490
|
+
payload.category = category;
|
|
491
|
+
if (defaultLocale !== undefined)
|
|
492
|
+
payload.defaultLocale = defaultLocale;
|
|
493
|
+
if (homepageUrl !== undefined)
|
|
494
|
+
payload.homepageUrl = homepageUrl;
|
|
495
|
+
if (supportUrl !== undefined)
|
|
496
|
+
payload.supportUrl = supportUrl;
|
|
497
|
+
if (Object.keys(payload).length === 0) {
|
|
498
|
+
throw new Error("No metadata fields provided.");
|
|
499
|
+
}
|
|
500
|
+
const result = await apiCall(url, {
|
|
501
|
+
method: "PUT",
|
|
502
|
+
headers: { "Content-Type": "application/json" },
|
|
503
|
+
body: JSON.stringify(payload),
|
|
504
|
+
});
|
|
505
|
+
return appendNote(formatResponse(result), V1_NOTE);
|
|
506
|
+
}
|
|
507
|
+
catch (e) {
|
|
508
|
+
return toolError(e);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
// ── update-metadata-ui (dashboard automation) ──
|
|
512
|
+
server.registerTool("update-metadata-ui", { description: descriptions["update-metadata-ui"], inputSchema: schemas["update-metadata-ui"] }, async ({ itemId, title, summary, description, category, homepageUrl, supportUrl, storeIconPath, accountIndex, headless }) => {
|
|
513
|
+
try {
|
|
514
|
+
const id = resolveItemId(itemId);
|
|
515
|
+
const idx = accountIndex ?? 0;
|
|
516
|
+
const dashboardUrl = `https://chromewebstore.google.com/u/${idx}/dashboard/${id}/edit`;
|
|
517
|
+
const hasAnyField = [title, summary, description, category, homepageUrl, supportUrl, storeIconPath].some((v) => typeof v === "string" && v.trim().length > 0);
|
|
518
|
+
if (!hasAnyField) {
|
|
519
|
+
throw new Error("No fields provided for UI update.");
|
|
520
|
+
}
|
|
521
|
+
const context = await chromium.launchPersistentContext(DASHBOARD_PROFILE_DIR, {
|
|
522
|
+
channel: "chrome",
|
|
523
|
+
headless: headless ?? false,
|
|
524
|
+
});
|
|
525
|
+
try {
|
|
526
|
+
const page = context.pages()[0] || (await context.newPage());
|
|
527
|
+
await page.goto(dashboardUrl, { waitUntil: "domcontentloaded", timeout: 90_000 });
|
|
528
|
+
await page.waitForTimeout(2500);
|
|
529
|
+
if (page.url().includes("accounts.google.com")) {
|
|
530
|
+
throw new Error(`Not signed in to Chrome Web Store dashboard. Open once with headless=false and sign in. Profile dir: ${DASHBOARD_PROFILE_DIR}`);
|
|
531
|
+
}
|
|
532
|
+
if (title?.trim()) {
|
|
533
|
+
await fillTextFieldByLabel(page, ["Title", "제목", "Name", "이름"], title.trim());
|
|
534
|
+
}
|
|
535
|
+
if (summary?.trim()) {
|
|
536
|
+
await fillTextFieldByLabel(page, ["Summary", "Short description", "요약", "짧은 설명"], summary.trim());
|
|
537
|
+
}
|
|
538
|
+
if (description?.trim()) {
|
|
539
|
+
await fillTextFieldByLabel(page, ["Description", "설명"], description.trim());
|
|
540
|
+
}
|
|
541
|
+
if (homepageUrl?.trim()) {
|
|
542
|
+
await fillTextFieldByLabel(page, ["Homepage", "홈페이지"], homepageUrl.trim());
|
|
543
|
+
}
|
|
544
|
+
if (supportUrl?.trim()) {
|
|
545
|
+
await fillTextFieldByLabel(page, ["Support", "지원", "Help", "도움말"], supportUrl.trim());
|
|
546
|
+
}
|
|
547
|
+
if (storeIconPath?.trim()) {
|
|
548
|
+
await uploadFileBySectionLabel(page, ["Store icon", "스토어 아이콘", "아이콘", "Icon"], storeIconPath.trim());
|
|
549
|
+
}
|
|
550
|
+
if (category?.trim()) {
|
|
551
|
+
const categoryCombo = page.getByRole("combobox", { name: /category|카테고리/i }).first();
|
|
552
|
+
if ((await categoryCombo.count()) > 0) {
|
|
553
|
+
await categoryCombo.click();
|
|
554
|
+
const option = page.getByRole("option", { name: new RegExp(escapeRegExp(category), "i") }).first();
|
|
555
|
+
if ((await option.count()) > 0) {
|
|
556
|
+
await option.click();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
await clickSaveButton(page);
|
|
561
|
+
return {
|
|
562
|
+
content: [
|
|
563
|
+
{
|
|
564
|
+
type: "text",
|
|
565
|
+
text: JSON.stringify({ ok: true, mode: "dashboard-ui", profileDir: DASHBOARD_PROFILE_DIR, url: page.url() }, null, 2),
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
isError: false,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
finally {
|
|
572
|
+
await context.close();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
catch (e) {
|
|
576
|
+
return toolError(e);
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
// ── Resources ──
|
|
580
|
+
server.registerResource("extension-status", new ResourceTemplate("cws://extensions/{extensionId}", { list: undefined }), {
|
|
581
|
+
title: "Chrome Web Store extension status",
|
|
582
|
+
description: "Get the current status (v2) and store-listing metadata (v1.1, while available) of a Chrome Web Store extension by its item ID.",
|
|
583
|
+
mimeType: "application/json",
|
|
584
|
+
}, async (uri, variables) => {
|
|
585
|
+
const extensionId = String(variables.extensionId);
|
|
586
|
+
try {
|
|
587
|
+
const pub = resolvePublisherId();
|
|
588
|
+
const token = await getAccessToken();
|
|
589
|
+
// v2 status — the canonical source, must succeed.
|
|
590
|
+
const statusRes = await fetch(`${API_BASE}/v2/publishers/${pub}/items/${extensionId}:fetchStatus`, {
|
|
591
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
592
|
+
});
|
|
593
|
+
const statusText = await statusRes.text();
|
|
594
|
+
let statusData;
|
|
595
|
+
try {
|
|
596
|
+
statusData = JSON.parse(statusText);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
statusData = { raw: statusText };
|
|
600
|
+
}
|
|
601
|
+
// v1.1 metadata — optional, gracefully degrades once the v1.1 API is sunset.
|
|
602
|
+
let metaData;
|
|
603
|
+
try {
|
|
604
|
+
const metaRes = await fetch(`${V1_BASE}/items/${extensionId}?projection=PUBLISHED`, {
|
|
605
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
606
|
+
});
|
|
607
|
+
const metaText = await metaRes.text();
|
|
608
|
+
try {
|
|
609
|
+
metaData = JSON.parse(metaText);
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
metaData = { raw: metaText };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
catch (e) {
|
|
616
|
+
metaData = {
|
|
617
|
+
unavailable: `v1.1 metadata fetch failed (the v1.1 API is deprecated after ${V1_SUNSET}): ${errMsg(e)}`,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const result = { extensionId, status: statusData, metadata: metaData };
|
|
621
|
+
return {
|
|
622
|
+
contents: [
|
|
623
|
+
{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(result, null, 2) },
|
|
624
|
+
],
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
catch (e) {
|
|
628
|
+
return {
|
|
629
|
+
contents: [
|
|
630
|
+
{ uri: uri.href, mimeType: "application/json", text: JSON.stringify({ extensionId, error: errMsg(e) }) },
|
|
631
|
+
],
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
// ── Prompts ──
|
|
636
|
+
server.registerPrompt("publish_extension", {
|
|
637
|
+
description: "Step-by-step guide for publishing or updating a Chrome extension on the Chrome Web Store. Walks through upload, metadata update, and publish steps.",
|
|
638
|
+
argsSchema: {
|
|
639
|
+
extensionId: z.string().describe("The Chrome Web Store extension item ID"),
|
|
640
|
+
zipPath: z.string().describe("Absolute path to the built extension ZIP file"),
|
|
641
|
+
version: z.string().optional().describe("New version string (e.g. '1.2.0') for context"),
|
|
642
|
+
},
|
|
643
|
+
}, ({ extensionId, zipPath, version }) => ({
|
|
644
|
+
messages: [
|
|
645
|
+
{
|
|
646
|
+
role: "user",
|
|
647
|
+
content: {
|
|
648
|
+
type: "text",
|
|
649
|
+
text: `Please help me publish my Chrome extension to the Chrome Web Store.
|
|
650
|
+
|
|
651
|
+
Extension ID: ${extensionId}
|
|
652
|
+
ZIP file: ${zipPath}${version ? `\nNew version: ${version}` : ""}
|
|
653
|
+
|
|
654
|
+
Follow these steps using the available cws-mcp tools:
|
|
655
|
+
|
|
656
|
+
1. **Upload the ZIP** — Use the \`upload\` tool with zipPath="${zipPath}" and itemId="${extensionId}" to upload the new build as a draft.
|
|
657
|
+
2. **Verify upload** — Use the \`status\` tool to confirm the upload succeeded and the item is in DRAFT state.
|
|
658
|
+
3. **Check/update metadata** — Use the \`get\` tool (projection=DRAFT) to review current listing metadata. If anything needs updating (title, description, category), prefer \`update-metadata-ui\` (the v1.1-based \`update-metadata\` is deprecated and sunset ${V1_SUNSET}).
|
|
659
|
+
4. **Publish** — Use the \`publish\` tool to submit the draft for review. Optionally use publishType="STAGED_PUBLISH" for staged rollout, or skipReview=true if eligible.
|
|
660
|
+
5. **Confirm submission** — Use the \`status\` tool again to confirm the item entered the review queue.
|
|
661
|
+
6. **Optional staged rollout** — After approval, use \`deploy-percentage\` to gradually roll out (e.g., 10%, 50%, 100%).
|
|
662
|
+
|
|
663
|
+
Please start with step 1 now.`,
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
],
|
|
667
|
+
}));
|
|
668
|
+
server.registerPrompt("check_status", {
|
|
669
|
+
description: "Check the review status and deployment percentage of a Chrome extension, and surface any actionable next steps.",
|
|
670
|
+
argsSchema: {
|
|
671
|
+
extensionId: z.string().describe("The Chrome Web Store extension item ID"),
|
|
672
|
+
},
|
|
673
|
+
}, ({ extensionId }) => ({
|
|
674
|
+
messages: [
|
|
675
|
+
{
|
|
676
|
+
role: "user",
|
|
677
|
+
content: {
|
|
678
|
+
type: "text",
|
|
679
|
+
text: `Please check the current status of my Chrome extension.
|
|
680
|
+
|
|
681
|
+
Extension ID: ${extensionId}
|
|
682
|
+
|
|
683
|
+
Use the following cws-mcp tools to gather a full picture:
|
|
684
|
+
|
|
685
|
+
1. **Fetch status** — Use the \`status\` tool with itemId="${extensionId}" to get the review status and any rejection reasons.
|
|
686
|
+
2. **Fetch metadata** — Use the \`get\` tool with itemId="${extensionId}" and projection=PUBLISHED to see what is currently live (v1.1 API, sunset ${V1_SUNSET}).
|
|
687
|
+
3. **Summarize** — Report:
|
|
688
|
+
- Current review state (e.g., IN_REVIEW, PUBLISHED, REJECTED, DRAFT)
|
|
689
|
+
- Deployed version and deploy percentage if in staged rollout
|
|
690
|
+
- Any rejection reason or action required
|
|
691
|
+
- Recommended next steps (e.g., fix policy violations, increase deploy-percentage, or no action needed)
|
|
692
|
+
|
|
693
|
+
Please start with step 1 now.`,
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
],
|
|
697
|
+
}));
|
|
698
|
+
// ── Start ──
|
|
699
|
+
async function main() {
|
|
700
|
+
const transport = new StdioServerTransport();
|
|
701
|
+
await server.connect(transport);
|
|
702
|
+
}
|
|
703
|
+
main().catch((err) => {
|
|
704
|
+
process.stderr.write(`Fatal: ${errMsg(err)}\n`);
|
|
705
|
+
process.exit(1);
|
|
706
|
+
});
|
|
707
|
+
// ── Smithery Sandbox ──
|
|
708
|
+
// Reuses the shared schemas/descriptions so tool definitions stay in one place.
|
|
709
|
+
export function createSandboxServer() {
|
|
710
|
+
const sandbox = new McpServer({
|
|
711
|
+
name: "cws-mcp",
|
|
712
|
+
version: VERSION,
|
|
713
|
+
});
|
|
714
|
+
const noop = async () => ({
|
|
715
|
+
content: [{ type: "text", text: "sandbox" }],
|
|
716
|
+
isError: false,
|
|
717
|
+
});
|
|
718
|
+
for (const name of Object.keys(schemas)) {
|
|
719
|
+
sandbox.registerTool(name, { description: descriptions[name], inputSchema: schemas[name] }, noop);
|
|
720
|
+
}
|
|
721
|
+
return sandbox;
|
|
722
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@1llum1n4t1/cws-mcp",
|
|
3
|
+
"version": "1.4.2",
|
|
4
|
+
"mcpName": "io.github.1llum1n4t1s/cws",
|
|
5
|
+
"description": "MCP server for Chrome Web Store extension management (V2 API)",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"cws-mcp": "dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/**/*",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"start": "node dist/index.js",
|
|
22
|
+
"dev": "tsx src/index.ts"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"chrome-web-store",
|
|
27
|
+
"chrome-extension",
|
|
28
|
+
"publish",
|
|
29
|
+
"model-context-protocol"
|
|
30
|
+
],
|
|
31
|
+
"author": "1llum1n4t1s",
|
|
32
|
+
"contributors": [
|
|
33
|
+
"mikusnuz (original author of cws-mcp)"
|
|
34
|
+
],
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"repository": {
|
|
40
|
+
"type": "git",
|
|
41
|
+
"url": "git+https://github.com/1llum1n4t1s/1llum1n4t1s.cws-mcp.git"
|
|
42
|
+
},
|
|
43
|
+
"homepage": "https://github.com/1llum1n4t1s/1llum1n4t1s.cws-mcp#readme",
|
|
44
|
+
"bugs": {
|
|
45
|
+
"url": "https://github.com/1llum1n4t1s/1llum1n4t1s.cws-mcp/issues"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
+
"playwright": "^1.60.0",
|
|
50
|
+
"zod": "^3.24.2"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.0.0",
|
|
54
|
+
"tsx": "^4.19.0",
|
|
55
|
+
"typescript": "^5.7.0"
|
|
56
|
+
}
|
|
57
|
+
}
|