noober-mcp 1.0.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/package.json +41 -0
- package/run.js +145 -0
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "noober-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for iOS app testing — see inside your running app: network calls, WebSocket frames, logs, UserDefaults, screen layout, and API mocking. Works with Claude Code, Cursor, Codex, Windsurf.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"noober-mcp": "./run.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"run.js",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"os": [
|
|
16
|
+
"darwin"
|
|
17
|
+
],
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"model-context-protocol",
|
|
21
|
+
"ios",
|
|
22
|
+
"testing",
|
|
23
|
+
"qa",
|
|
24
|
+
"noober",
|
|
25
|
+
"network-inspector",
|
|
26
|
+
"websocket",
|
|
27
|
+
"simulator",
|
|
28
|
+
"xcode",
|
|
29
|
+
"claude"
|
|
30
|
+
],
|
|
31
|
+
"author": "NoobQA",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"homepage": "https://noobqa.com",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/noob-programmer1/NooberMCP.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/noob-programmer1/NooberMCP/issues"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/run.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* noober-mcp — MCP server for iOS app testing.
|
|
5
|
+
*
|
|
6
|
+
* Downloads the NooberMCP release bundle for your platform and runs it.
|
|
7
|
+
* Works with Claude Code, Cursor, Codex, Windsurf — any MCP client.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx noober-mcp
|
|
11
|
+
*
|
|
12
|
+
* Add to Claude Code:
|
|
13
|
+
* claude mcp add noober -- npx -y noober-mcp
|
|
14
|
+
*
|
|
15
|
+
* Add to Cursor (.cursor/mcp.json):
|
|
16
|
+
* { "mcpServers": { "noober": { "command": "npx", "args": ["-y", "noober-mcp"] } } }
|
|
17
|
+
*
|
|
18
|
+
* Environment variables:
|
|
19
|
+
* NOOBER_PORT=8686 HTTP API port (default 8686)
|
|
20
|
+
* NOOBER_MCP_BIN=path Use a local NooberMCP build instead of downloading
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const { spawn, execFileSync } = require('child_process');
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const os = require('os');
|
|
26
|
+
const path = require('path');
|
|
27
|
+
const https = require('https');
|
|
28
|
+
|
|
29
|
+
const pkg = require('./package.json');
|
|
30
|
+
const VERSION = pkg.version;
|
|
31
|
+
const REPO = 'noob-programmer1/NooberMCP';
|
|
32
|
+
|
|
33
|
+
// The release asset is a tarball, not a bare binary: NooberMCP resolves AXe and the
|
|
34
|
+
// probe dylibs relative to its own location (<dir>/Bundled/...), so they must travel together.
|
|
35
|
+
const INSTALL_DIR = path.join(os.homedir(), '.noober-mcp', `v${VERSION}`);
|
|
36
|
+
const BINARY_PATH = path.join(INSTALL_DIR, 'NooberMCP');
|
|
37
|
+
|
|
38
|
+
function platformSlug() {
|
|
39
|
+
if (os.platform() !== 'darwin') return null;
|
|
40
|
+
return os.arch() === 'arm64' ? 'macos-arm64' : 'macos-x86_64';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function download(url, dest, redirectsLeft = 5) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
https
|
|
46
|
+
.get(url, { headers: { 'User-Agent': `noober-mcp/${VERSION}` } }, (response) => {
|
|
47
|
+
const { statusCode, headers } = response;
|
|
48
|
+
|
|
49
|
+
if (statusCode >= 300 && statusCode < 400 && headers.location) {
|
|
50
|
+
response.resume();
|
|
51
|
+
if (redirectsLeft === 0) return reject(new Error('too many redirects'));
|
|
52
|
+
return download(headers.location, dest, redirectsLeft - 1).then(resolve, reject);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (statusCode !== 200) {
|
|
56
|
+
response.resume();
|
|
57
|
+
return reject(new Error(`HTTP ${statusCode} for ${url}`));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const file = fs.createWriteStream(dest);
|
|
61
|
+
response.pipe(file);
|
|
62
|
+
file.on('error', reject);
|
|
63
|
+
file.on('finish', () => file.close(() => resolve()));
|
|
64
|
+
})
|
|
65
|
+
.on('error', reject);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function ensureBinary() {
|
|
70
|
+
const override = process.env.NOOBER_MCP_BIN;
|
|
71
|
+
if (override) {
|
|
72
|
+
if (!fs.existsSync(override)) {
|
|
73
|
+
console.error(`NOOBER_MCP_BIN points at a missing file: ${override}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
return override;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (fs.existsSync(BINARY_PATH)) return BINARY_PATH;
|
|
80
|
+
|
|
81
|
+
const slug = platformSlug();
|
|
82
|
+
if (!slug) {
|
|
83
|
+
console.error(
|
|
84
|
+
`noober-mcp supports macOS only (iOS simulators and Xcode live there). Detected ${os.platform()}/${os.arch()}.`
|
|
85
|
+
);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const asset = `noober-mcp-${slug}.tar.gz`;
|
|
90
|
+
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${asset}`;
|
|
91
|
+
const stagingDir = `${INSTALL_DIR}.tmp-${process.pid}`;
|
|
92
|
+
const tarball = path.join(os.tmpdir(), `${asset}.${process.pid}`);
|
|
93
|
+
|
|
94
|
+
console.error(`Downloading NooberMCP v${VERSION} (${slug})...`);
|
|
95
|
+
fs.mkdirSync(path.dirname(INSTALL_DIR), { recursive: true });
|
|
96
|
+
fs.mkdirSync(stagingDir, { recursive: true });
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await download(url, tarball);
|
|
100
|
+
|
|
101
|
+
// A truncated or HTML error body would otherwise be unpacked and executed.
|
|
102
|
+
const { size } = fs.statSync(tarball);
|
|
103
|
+
if (size < 1000000) throw new Error(`downloaded asset is only ${size} bytes`);
|
|
104
|
+
|
|
105
|
+
execFileSync('tar', ['-xzf', tarball, '-C', stagingDir]);
|
|
106
|
+
if (!fs.existsSync(path.join(stagingDir, 'NooberMCP'))) {
|
|
107
|
+
throw new Error('release archive is missing the NooberMCP binary');
|
|
108
|
+
}
|
|
109
|
+
fs.chmodSync(path.join(stagingDir, 'NooberMCP'), 0o755);
|
|
110
|
+
const bundledAxe = path.join(stagingDir, 'Bundled', 'axe');
|
|
111
|
+
if (fs.existsSync(bundledAxe)) fs.chmodSync(bundledAxe, 0o755);
|
|
112
|
+
|
|
113
|
+
fs.renameSync(stagingDir, INSTALL_DIR);
|
|
114
|
+
console.error(`Installed to ${INSTALL_DIR}`);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
117
|
+
console.error(`Failed to install NooberMCP: ${err.message}`);
|
|
118
|
+
console.error(`Expected asset: ${url}`);
|
|
119
|
+
console.error('Build from source instead: clone the repo, run swift build -c release,');
|
|
120
|
+
console.error('then NOOBER_MCP_BIN=/path/to/.build/release/NooberMCP npx noober-mcp');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
} finally {
|
|
123
|
+
fs.rmSync(tarball, { force: true });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return BINARY_PATH;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function main() {
|
|
130
|
+
const binaryPath = await ensureBinary();
|
|
131
|
+
|
|
132
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
133
|
+
stdio: 'inherit',
|
|
134
|
+
env: { ...process.env },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
child.on('exit', (code, signal) => {
|
|
138
|
+
if (signal) process.kill(process.pid, signal);
|
|
139
|
+
process.exit(code || 0);
|
|
140
|
+
});
|
|
141
|
+
process.on('SIGINT', () => child.kill('SIGINT'));
|
|
142
|
+
process.on('SIGTERM', () => child.kill('SIGTERM'));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
main();
|