appforshare-mcp 0.0.0-stage → 1.0.1
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 +42 -2
- package/dist/server.js +226 -0
- package/package.json +18 -4
package/README.md
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
-
#
|
|
1
|
+
# appforshare-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MCP server for [AppForShare](https://appforshare.io). Lets Claude Code, Cursor, Codex, VS Code, Gemini CLI and other
|
|
4
|
+
MCP clients upload iOS and Android builds, find install links, read the store readiness check and follow App Store
|
|
5
|
+
and Google Play releases and reviews.
|
|
6
|
+
|
|
7
|
+
Needs an AppForShare API token (Pro and Team), from https://appforshare.io/dashboard/settings, and Node 18 or newer.
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"appforshare": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "appforshare-mcp"],
|
|
15
|
+
"env": { "APPFORSHARE_TOKEN": "afs_your_token" }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Claude Code:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
claude mcp add appforshare --env APPFORSHARE_TOKEN=afs_your_token -- npx -y appforshare-mcp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
No local uploads needed? Use the hosted server instead: `https://appforshare.io/api/mcp` with the header
|
|
28
|
+
`Authorization: Bearer afs_your_token`. Setup for each client: https://appforshare.io/docs/mcp
|
|
29
|
+
|
|
30
|
+
## Tools
|
|
31
|
+
|
|
32
|
+
| Tool | What it does |
|
|
33
|
+
|---|---|
|
|
34
|
+
| `upload_build` | Upload a local .ipa, .apk or .aab (release notes, channel). Returns the install link and readiness result. |
|
|
35
|
+
| `list_builds`, `latest_build` | Builds with install links, filtered by platform, app, project or channel. |
|
|
36
|
+
| `build_info` | One build with every store readiness finding and its fix. |
|
|
37
|
+
| `list_projects`, `whoami` | Projects in the account; account, plan and role of the token. |
|
|
38
|
+
| `release_status`, `list_reviews` | App Store and Google Play release status and reviews. Read only. |
|
|
39
|
+
| `promote_build`, `delete_build` | Change a channel or delete a build. Developer role and `confirm: true` required. |
|
|
40
|
+
|
|
41
|
+
Environment: `APPFORSHARE_TOKEN` (required), `APPFORSHARE_API` (default `https://appforshare.io`).
|
|
42
|
+
|
|
43
|
+
MIT licence.
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
// AppForShare MCP server over stdio. Every tool of the hosted server (https://appforshare.io/api/mcp) is passed
|
|
5
|
+
// through with the same API token, plus `upload_build`, which reads a file from this computer and uploads it
|
|
6
|
+
// with the same resumable API as the CLI. Only JSON-RPC goes to stdout; anything else goes to stderr.
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
const node_readline_1 = require("node:readline");
|
|
10
|
+
const VERSION = '1.0.1';
|
|
11
|
+
const TOKEN = process.env.APPFORSHARE_TOKEN ?? '';
|
|
12
|
+
const API = (process.env.APPFORSHARE_API ?? 'https://appforshare.io').replace(/\/$/, '');
|
|
13
|
+
const UA = `appforshare-mcp/${VERSION}`;
|
|
14
|
+
class ToolError extends Error {
|
|
15
|
+
}
|
|
16
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
17
|
+
const send = (m) => void process.stdout.write(`${JSON.stringify(m)}\n`);
|
|
18
|
+
const log = (s) => process.stderr.write(`appforshare-mcp: ${s}\n`);
|
|
19
|
+
const UPLOAD_TOOL = {
|
|
20
|
+
name: 'upload_build',
|
|
21
|
+
title: 'Upload a build',
|
|
22
|
+
description: 'Upload an .ipa, .apk or .aab from this computer to AppForShare and return its install link and store readiness result. An .aab is turned into one APK for testers. Optional release notes and a channel (such as beta) whose stable link then opens this build.',
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
path: { type: 'string', description: 'Path to the .ipa, .apk or .aab file' },
|
|
27
|
+
notes: { type: 'string', description: 'Release notes shown to testers (Pro and Team)' },
|
|
28
|
+
channel: { type: 'string', description: 'Put the build into this channel when it is ready (Pro and Team)' },
|
|
29
|
+
},
|
|
30
|
+
required: ['path'],
|
|
31
|
+
},
|
|
32
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
33
|
+
};
|
|
34
|
+
/** One call to the hosted MCP endpoint. */
|
|
35
|
+
async function remote(msg) {
|
|
36
|
+
if (!TOKEN)
|
|
37
|
+
throw new ToolError('APPFORSHARE_TOKEN is not set. Create an API token at https://appforshare.io/dashboard/settings and add it to this MCP server\'s env.');
|
|
38
|
+
const res = await fetch(`${API}/api/mcp`, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { Authorization: `Bearer ${TOKEN}`, 'Content-Type': 'application/json', Accept: 'application/json, text/event-stream', 'User-Agent': UA },
|
|
41
|
+
body: JSON.stringify(msg),
|
|
42
|
+
});
|
|
43
|
+
const data = (await res.json().catch(() => null));
|
|
44
|
+
if (data && 'jsonrpc' in data)
|
|
45
|
+
return data;
|
|
46
|
+
throw new ToolError(data?.message ?? `AppForShare answered ${res.status}`);
|
|
47
|
+
}
|
|
48
|
+
async function rest(method, path, opts = {}) {
|
|
49
|
+
for (let attempt = 1;; attempt++) {
|
|
50
|
+
let res;
|
|
51
|
+
try {
|
|
52
|
+
res = await fetch(`${API}/api${path}`, {
|
|
53
|
+
method,
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: `Bearer ${TOKEN}`,
|
|
56
|
+
'User-Agent': UA,
|
|
57
|
+
...(opts.json !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
58
|
+
...(opts.body ? { 'Content-Type': 'application/octet-stream' } : {}),
|
|
59
|
+
...opts.headers,
|
|
60
|
+
},
|
|
61
|
+
body: opts.json !== undefined ? JSON.stringify(opts.json) : opts.body ? new Uint8Array(opts.body) : undefined,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
if (attempt >= 6)
|
|
66
|
+
throw new ToolError(`Network error: ${err.message}`);
|
|
67
|
+
await sleep(Math.min(30_000, 1000 * 2 ** attempt));
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const data = (await res.json().catch(() => ({})));
|
|
71
|
+
if (res.status >= 500 && attempt < 6) {
|
|
72
|
+
await sleep(Math.min(30_000, 1000 * 2 ** attempt));
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (!res.ok && res.status !== 409 && !(res.status === 400 && typeof data.offset === 'number')) {
|
|
76
|
+
const msg = Array.isArray(data.message) ? data.message[0] : data.message;
|
|
77
|
+
throw new ToolError(msg || `Request failed with ${res.status}`);
|
|
78
|
+
}
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function readChunk(fd, offset, length) {
|
|
83
|
+
const buf = Buffer.alloc(length);
|
|
84
|
+
let read = 0;
|
|
85
|
+
while (read < length) {
|
|
86
|
+
const n = (0, node_fs_1.readSync)(fd, buf, read, length - read, offset + read);
|
|
87
|
+
if (!n)
|
|
88
|
+
break;
|
|
89
|
+
read += n;
|
|
90
|
+
}
|
|
91
|
+
return buf.subarray(0, read);
|
|
92
|
+
}
|
|
93
|
+
async function upload(args) {
|
|
94
|
+
if (!TOKEN)
|
|
95
|
+
throw new ToolError('APPFORSHARE_TOKEN is not set. Create an API token at https://appforshare.io/dashboard/settings.');
|
|
96
|
+
if (typeof args.path !== 'string' || !args.path.trim())
|
|
97
|
+
throw new ToolError('path is required');
|
|
98
|
+
const file = (0, node_path_1.resolve)(args.path);
|
|
99
|
+
if (!/\.(ipa|apk|aab)$/i.test(file))
|
|
100
|
+
throw new ToolError('Only .ipa, .apk and .aab files can be shared. An .aab is turned into an APK for testers.');
|
|
101
|
+
let size;
|
|
102
|
+
try {
|
|
103
|
+
size = (0, node_fs_1.statSync)(file).size;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
throw new ToolError(`File not found: ${file}`);
|
|
107
|
+
}
|
|
108
|
+
const notes = typeof args.notes === 'string' && args.notes.trim() ? args.notes.slice(0, 5000) : undefined;
|
|
109
|
+
const channel = typeof args.channel === 'string' && args.channel.trim() ? args.channel.trim() : undefined;
|
|
110
|
+
const created = await rest('POST', '/uploads', {
|
|
111
|
+
json: { fileName: (0, node_path_1.basename)(file), size, ...(notes ? { releaseNotes: notes } : {}), ...(channel ? { channel } : {}) },
|
|
112
|
+
});
|
|
113
|
+
const fd = (0, node_fs_1.openSync)(file, 'r');
|
|
114
|
+
let offset = created.offset;
|
|
115
|
+
try {
|
|
116
|
+
while (offset < size) {
|
|
117
|
+
const chunk = readChunk(fd, offset, Math.min(created.chunkBytes, size - offset));
|
|
118
|
+
const d = await rest('PUT', `/uploads/${created.id}?offset=${offset}`, { body: chunk, headers: { 'X-Upload-Token': created.token } });
|
|
119
|
+
offset = typeof d.offset === 'number' ? d.offset : offset + chunk.length;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
(0, node_fs_1.closeSync)(fd);
|
|
124
|
+
}
|
|
125
|
+
const h = { headers: { 'X-Upload-Token': created.token } };
|
|
126
|
+
await rest('POST', `/uploads/${created.id}/complete`, h);
|
|
127
|
+
const deadline = Date.now() + 20 * 60 * 1000;
|
|
128
|
+
let s;
|
|
129
|
+
for (;;) {
|
|
130
|
+
s = await rest('GET', `/uploads/${created.id}`, h);
|
|
131
|
+
if (s.status === 'READY' || s.status === 'FAILED')
|
|
132
|
+
break;
|
|
133
|
+
if (Date.now() > deadline)
|
|
134
|
+
throw new ToolError('Processing is taking too long. Check the AppForShare dashboard.');
|
|
135
|
+
await sleep(2000);
|
|
136
|
+
}
|
|
137
|
+
if (s.status === 'FAILED')
|
|
138
|
+
throw new ToolError(s.error ?? 'The build could not be processed.');
|
|
139
|
+
for (let i = 0; channel && !s.channel && i < 5; i++) {
|
|
140
|
+
await sleep(1000);
|
|
141
|
+
s = await rest('GET', `/uploads/${created.id}`, h);
|
|
142
|
+
}
|
|
143
|
+
let readiness = null;
|
|
144
|
+
try {
|
|
145
|
+
readiness = (await rest('GET', `/ci/builds/${encodeURIComponent(s.slug ?? '')}`)).readinessReport;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Readiness is extra; the upload worked.
|
|
149
|
+
}
|
|
150
|
+
return { url: s.url, slug: s.slug, name: s.build?.name, version: s.build?.version, expiresAt: s.expiresAt, channel: s.channel, readiness };
|
|
151
|
+
}
|
|
152
|
+
async function handle(m) {
|
|
153
|
+
const reply = (result) => send({ jsonrpc: '2.0', id: m.id, result });
|
|
154
|
+
const fail = (code, message) => send({ jsonrpc: '2.0', id: m.id ?? null, error: { code, message } });
|
|
155
|
+
if (m.id === undefined)
|
|
156
|
+
return; // notifications need no answer
|
|
157
|
+
try {
|
|
158
|
+
switch (m.method) {
|
|
159
|
+
case 'initialize': {
|
|
160
|
+
const asked = typeof m.params?.protocolVersion === 'string' ? m.params.protocolVersion : '2025-06-18';
|
|
161
|
+
if (!TOKEN)
|
|
162
|
+
log('APPFORSHARE_TOKEN is not set; tools will answer with an error until it is.');
|
|
163
|
+
return reply({
|
|
164
|
+
protocolVersion: asked,
|
|
165
|
+
capabilities: { tools: { listChanged: false } },
|
|
166
|
+
serverInfo: { name: 'appforshare', title: 'AppForShare', version: VERSION, websiteUrl: 'https://appforshare.io/docs/mcp' },
|
|
167
|
+
instructions: 'AppForShare shares iOS and Android test builds by install link and tracks App Store and Google Play releases. upload_build uploads a local .ipa or .apk. ' +
|
|
168
|
+
'promote_build and delete_build change things: ask the person first and pass confirm: true.',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
case 'ping':
|
|
172
|
+
return reply({});
|
|
173
|
+
case 'tools/list': {
|
|
174
|
+
let tools = [];
|
|
175
|
+
try {
|
|
176
|
+
const r = await remote({ jsonrpc: '2.0', id: 1, method: 'tools/list' });
|
|
177
|
+
tools = (r.result?.tools ?? []);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
log(err.message);
|
|
181
|
+
}
|
|
182
|
+
return reply({ tools: [UPLOAD_TOOL, ...tools] });
|
|
183
|
+
}
|
|
184
|
+
case 'tools/call': {
|
|
185
|
+
if (m.params?.name === UPLOAD_TOOL.name) {
|
|
186
|
+
try {
|
|
187
|
+
const data = await upload((m.params.arguments ?? {}));
|
|
188
|
+
return reply({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], structuredContent: data, isError: false });
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
return reply({ content: [{ type: 'text', text: err instanceof ToolError ? err.message : String(err) }], isError: true });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const r = await remote({ jsonrpc: '2.0', id: m.id ?? 1, method: 'tools/call', params: m.params });
|
|
196
|
+
return r.error ? send({ ...r, id: m.id }) : reply(r.result);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
return reply({ content: [{ type: 'text', text: err.message }], isError: true });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
default:
|
|
203
|
+
return fail(-32601, `Method not found: ${m.method}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
fail(-32603, err.message);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
211
|
+
process.stdout.write(`${VERSION}\n`);
|
|
212
|
+
process.exit(0);
|
|
213
|
+
}
|
|
214
|
+
const rl = (0, node_readline_1.createInterface)({ input: process.stdin, crlfDelay: Infinity });
|
|
215
|
+
rl.on('line', (line) => {
|
|
216
|
+
if (!line.trim())
|
|
217
|
+
return;
|
|
218
|
+
let m;
|
|
219
|
+
try {
|
|
220
|
+
m = JSON.parse(line);
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } });
|
|
224
|
+
}
|
|
225
|
+
void handle(m);
|
|
226
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appforshare-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "AppForShare MCP server for Claude Code, Cursor, Codex and other AI tools: upload iOS and Android builds, get install links, check store readiness and follow App Store and Google Play releases.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "AppForShare",
|
|
7
|
+
"homepage": "https://appforshare.io/docs/mcp",
|
|
8
|
+
"keywords": ["mcp", "model-context-protocol", "ios", "android", "ipa", "apk", "testflight", "app-distribution", "beta-testing"],
|
|
9
|
+
"bin": { "appforshare-mcp": "dist/server.js" },
|
|
10
|
+
"files": ["dist", "README.md"],
|
|
11
|
+
"engines": { "node": ">=18" },
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/server.js",
|
|
14
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22",
|
|
18
|
+
"typescript": "~5.9.3"
|
|
19
|
+
}
|
|
20
|
+
}
|