@mmerterden/multi-agent-toolkit-mcp 3.9.0 → 3.12.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/CHANGELOG.md +239 -0
- package/README.md +79 -2
- package/README.tr.md +6 -3
- package/index.js +160 -21
- package/package.json +9 -3
- package/tools/code-intel/index.js +665 -0
- package/tools/code-intel/kotlin.js +159 -0
- package/tools/code-intel/lsp-client.js +422 -0
- package/tools/code-intel/pool.js +273 -0
- package/tools/code-intel/positions.js +195 -0
- package/tools/code-intel/swift.js +249 -0
- package/tools/ios-app-store-audit/context.js +160 -9
- package/tools/ios-app-store-audit/index.js +24 -1
- package/tools/offload/index.js +28 -8
- package/tools/pass-kit/index.js +432 -0
- package/tools/pass-kit/sign.js +255 -0
- package/tools/pass-kit/spec.js +317 -0
- package/tools/pass-kit/validate.js +329 -0
package/index.js
CHANGED
|
@@ -29,6 +29,15 @@ import {
|
|
|
29
29
|
validateApp,
|
|
30
30
|
} from "./tools/ios-testflight/index.js";
|
|
31
31
|
import { DESIGN_TOOLS, handleDesign } from "./tools/design-check/index.js";
|
|
32
|
+
import {
|
|
33
|
+
CODE_TOOLS,
|
|
34
|
+
handleCode,
|
|
35
|
+
CODE_READ_ONLY,
|
|
36
|
+
CODE_IDEMPOTENT,
|
|
37
|
+
CODE_OUTPUT_SCHEMAS,
|
|
38
|
+
shutdownAllLsp,
|
|
39
|
+
} from "./tools/code-intel/index.js";
|
|
40
|
+
import { PASS_TOOLS, handlePass, PASS_READ_ONLY, PASS_OUTPUT_SCHEMAS } from "./tools/pass-kit/index.js";
|
|
32
41
|
import { parseLaunchOutput } from "./tools/launch-time/index.js";
|
|
33
42
|
import { parseLeaksOutput, parseMeminfoOutput, diffMeminfo } from "./tools/memory/index.js";
|
|
34
43
|
import { auditIosTree, auditAndroidDump, parseAuditResults } from "./tools/a11y/index.js";
|
|
@@ -475,7 +484,7 @@ const IOS_TOOLS = [
|
|
|
475
484
|
{ name: "ios_export_ipa", description: "Export a .xcarchive to a signed .ipa via xcodebuild -exportArchive. Generates the exportOptions.plist from the arguments (method defaults to app-store-connect), so callers do not have to hand-maintain one. Returns the .ipa path plus parsed errors; a run that exits 0 without producing an .ipa is reported as a failure. Pair with ios_testflight_validate for the pre-submission gate.", inputSchema: { type: "object", properties: { archive_path: { type: "string", description: "Absolute path to the .xcarchive" }, output_dir: { type: "string", description: "Directory to write the .ipa into" }, method: { type: "string", description: "Export method: app-store-connect (default) | release-testing | enterprise | development" }, team_id: { type: "string", description: "Apple Developer team ID" }, provisioning_profiles: { type: "object", description: "Map of bundleId -> provisioning profile name (manual signing)" }, signing_style: { type: "string", description: "automatic | manual" }, upload_symbols: { type: "boolean", description: "Include symbols (default true)" }, allow_provisioning_updates: { type: "boolean", description: "Off by default. Lets xcodebuild register devices and create/modify provisioning profiles in the developer account - a change on Apple's side, so it is opt-in" }, timeout_sec: { type: "number", description: "Default 900" } }, required: ["archive_path", "output_dir"] } },
|
|
476
485
|
{ name: "ios_testflight_validate", description: "Run Apple's own pre-submission validation on an .ipa via `xcrun altool --validate-app`, then map returned ITMS error codes onto the App Store rule each one implies. This is the authoritative gate: unlike the static ios_app_store_audit it can catch an unregistered bundle ID, a profile that does not match the App Store Connect app record, a version+build pair already used, and entitlements not provisioned for the app ID. Auth is a 3-tier chain: ASC API key (api_key_id + api_issuer_id), else Apple ID + app-specific password referenced indirectly through a keychain item or env var (never passed by value), else the gate returns verdict SKIPPED with a reason. SKIPPED is not a pass - it means Apple was never asked. Set list_providers=true for a pre-flight that reports which teams the credentials can deliver for (needed when a corporate Apple ID belongs to several).", inputSchema: { type: "object", properties: { ipa_path: { type: "string", description: "Absolute path to the .ipa" }, platform: { type: "string", description: "ios (default) | appletvos | visionos | macos" }, api_key_id: { type: "string", description: "App Store Connect API key ID (tier 1)" }, api_issuer_id: { type: "string", description: "App Store Connect issuer ID (tier 1)" }, p8_path: { type: "string", description: "Explicit path to AuthKey_<id>.p8; otherwise altool's search dirs are used" }, apple_id: { type: "string", description: "Apple ID (tier 2)" }, keychain_item: { type: "string", description: "Keychain item holding the app-specific password (tier 2, preferred)" }, password_env_var: { type: "string", description: "Env var holding the app-specific password (tier 2 fallback)" }, provider_public_id: { type: "string", description: "Required when the account belongs to multiple providers" }, list_providers: { type: "boolean", description: "Pre-flight only: list deliverable providers and return" }, timeout_sec: { type: "number", description: "Default 900" } }, required: [] } },
|
|
477
486
|
{ name: "ios_app_store_audit", description: "Deep App Store Review compliance audit for .xcarchive bundles. 18-rule catalog covering privacy manifest, code signing, embedded SDKs, entitlements, asset hygiene, IPv6 compliance, debug-tool leak detection, Swift ABI compatibility, SDK floor (ITMS-90725), and more. Returns structured JSON with severity-ranked violations and ITMS error code mappings. Replaces ios_archive_audit (deprecated). Pass rules='core' for the 6 baseline checks (code-signing, entitlements, info-plist, privacy-manifest, binary-size, sdk-floor); 'all'|'deep' for the full 18-rule scan; CSV like 'binary-size,ipv6-compliance' for an explicit subset.", inputSchema: { type: "object", properties: { archive_path: { type: "string", description: "Absolute path to the .xcarchive bundle" }, rules: { type: "string", description: "'all' (default) | 'core' | 'deep' | comma-separated ruleIDs" } }, required: ["archive_path"] } },
|
|
478
|
-
{ name: "ios_xcodebuild", description: "Build / test / clean an Xcode project with progressive disclosure. Returns one-line summary plus xcresult ID; drill in via ios_xcresult. Token-efficient - full log stays out of context unless requested.", inputSchema: { type: "object", properties: { project: { type: "string", description: "Path to .xcodeproj (mutually exclusive with workspace)" }, workspace: { type: "string", description: "Path to .xcworkspace (mutually exclusive with project)" }, scheme: { type: "string" }, configuration: { type: "string", description: "Debug / Release (default: Release)" }, destination: { type: "string", description: "Xcode destination string. Default: generic iOS Simulator; for action test, the booted simulator (xcodebuild refuses a generic destination for test)" }, action: { type: "string", enum: ["build", "test", "clean", "archive", "clean-build"], description: "Default: build" }, derived_data_path: { type: "string" }, extra_args: { type: "string", description: "Additional raw xcodebuild args appended verbatim" }, timeout_sec: { type: "number", description: "Build timeout in seconds (default 600)" } }, required: ["scheme"] } },
|
|
487
|
+
{ name: "ios_xcodebuild", description: "Build / test / clean an Xcode project with progressive disclosure. Returns one-line summary plus xcresult ID; drill in via ios_xcresult. Token-efficient - full log stays out of context unless requested.", inputSchema: { type: "object", properties: { project: { type: "string", description: "Path to .xcodeproj (mutually exclusive with workspace)" }, workspace: { type: "string", description: "Path to .xcworkspace (mutually exclusive with project)" }, scheme: { type: "string" }, configuration: { type: "string", description: "Debug / Release (default: Release)" }, destination: { type: "string", description: "Xcode destination string. Default: generic iOS Simulator; for action test, the booted simulator (xcodebuild refuses a generic destination for test)" }, action: { type: "string", enum: ["build", "test", "clean", "archive", "clean-build"], description: "Default: build" }, derived_data_path: { type: "string" }, extra_args: { type: "string", description: "Additional raw xcodebuild args appended verbatim. Plain flags and values only - shell metacharacters are refused.", pattern: "^[^;&|`$(){}<>\\\\\\n]*$", patternHint: "may contain only plain flags and values (no shell metacharacters ; & | ` $ ( ) { } < > \\\\)" }, timeout_sec: { type: "number", description: "Build timeout in seconds (default 600)" } }, required: ["scheme"] } },
|
|
479
488
|
{ name: "ios_xcresult", description: "Drill into a previous ios_xcodebuild result by xcresult ID. Modes: summary (counts), errors (file:line + message), warnings, log (last N lines), tests (failed), metrics (XCTMetric performance results as JSON). Use this instead of dumping the whole build log into context.", inputSchema: { type: "object", properties: { id: { type: "string", description: "xcresult ID returned by ios_xcodebuild" }, mode: { type: "string", enum: ["summary", "errors", "warnings", "log", "tests", "metrics"], description: "Default: summary" }, log_lines: { type: "number", description: "Lines of raw log to return when mode=log (default 200)" }, test_id: { type: "string", description: "mode=metrics only: scope to one test case or suite instead of every measured test" } }, required: ["id"] } },
|
|
480
489
|
{ name: "ios_visual_diff", description: "Compare two PNG screenshots. Returns JSON with diff_pct, pass/fail vs threshold, and an optional diff image. Use for snapshot regression checks across light/dark, locale, dynamic type variants.", inputSchema: { type: "object", properties: { baseline: { type: "string", description: "Path to baseline PNG" }, current: { type: "string", description: "Path to current PNG" }, threshold: { type: "number", description: "Per-pixel color threshold 0..1 (default 0.1, lower = stricter)" }, max_diff_pct: { type: "number", description: "Fail if diff exceeds this percent (default 1.0)" }, output: { type: "string", description: "Path to write diff PNG (optional)" } }, required: ["baseline", "current"] } },
|
|
481
490
|
{ name: "ios_leaks", description: "Look for leaked memory in a running simulator (or host) process with /usr/bin/leaks. mode=snapshot reports the current leak count and bytes; mode=diff reports only leaks new since a saved memory graph, which is the shape a regression gate wants. Reports measurable:false when the target lacks get-task-allow rather than reporting it as clean - leaks exits 0 in that case, so an unmeasurable target and a clean one are indistinguishable by exit status. Debug builds are debuggable; Apple-signed apps are not.", inputSchema: { type: "object", properties: { pid: { type: "number", description: "Process id. Either this or bundle_id." }, bundle_id: { type: "string", description: "Bundle id of an app running on the booted simulator; its pid is resolved for you." }, device_id: { type: "string" }, mode: { type: "string", enum: ["snapshot", "diff"], description: "Default: snapshot" }, baseline_graph: { type: "string", description: "mode=diff: path to the memory graph saved by an earlier call" }, output_graph: { type: "string", description: "Save a memory graph here to use as a later baseline" } }, required: [] } },
|
|
@@ -487,7 +496,38 @@ const IOS_TOOLS = [
|
|
|
487
496
|
// reports needs no simulator, so a missing Xcode must not refuse them.
|
|
488
497
|
const IOS_TOOLS_WITHOUT_XCRUN = new Set(["ios_visual_diff", "ios_list_crashes", "ios_xcresult"]);
|
|
489
498
|
|
|
499
|
+
// Argument rules a JSON Schema cannot state, because they relate two fields
|
|
500
|
+
// rather than constrain one. They ran inside each handler, which put them
|
|
501
|
+
// BEHIND the "Xcode not installed" gate: on a host without Xcode every
|
|
502
|
+
// malformed call got the same answer as a well-formed one, and the tests that
|
|
503
|
+
// prove these rules could not run there at all. That is how the Linux CI leg
|
|
504
|
+
// found them. They answer from the arguments alone, so they belong in front of
|
|
505
|
+
// the capability gate - a caller learns their call is wrong whether or not this
|
|
506
|
+
// machine could have run it.
|
|
507
|
+
const IOS_ARG_RULES = {
|
|
508
|
+
ios_leaks: (a) => {
|
|
509
|
+
if (!a.pid && !a.bundle_id) return "pass pid or bundle_id";
|
|
510
|
+
if ((a.mode || "snapshot") === "diff" && !a.baseline_graph) {
|
|
511
|
+
return "mode=diff needs baseline_graph, the memory graph saved by an earlier call";
|
|
512
|
+
}
|
|
513
|
+
return null;
|
|
514
|
+
},
|
|
515
|
+
ios_xcodebuild: (a) => {
|
|
516
|
+
if (!a.project && !a.workspace) return "project or workspace required";
|
|
517
|
+
if (a.project && a.workspace) return "pass project OR workspace, not both";
|
|
518
|
+
return null;
|
|
519
|
+
},
|
|
520
|
+
ios_accessibility_audit_deep: (a) => (!a.project && !a.workspace ? "pass project or workspace" : null),
|
|
521
|
+
ios_testflight_validate: (a) =>
|
|
522
|
+
!a.list_providers && !a.ipa_path ? "ipa_path is required (or pass list_providers=true)" : null,
|
|
523
|
+
};
|
|
524
|
+
|
|
490
525
|
async function handleIOS(name, args, ctx = {}) {
|
|
526
|
+
const argRule = IOS_ARG_RULES[name];
|
|
527
|
+
if (argRule) {
|
|
528
|
+
const bad = argRule(args);
|
|
529
|
+
if (bad) return `${ERROR_PREFIX}${bad}`;
|
|
530
|
+
}
|
|
491
531
|
if (!HAS_XCRUN && !IOS_TOOLS_WITHOUT_XCRUN.has(name)) return `${ERROR_PREFIX}Xcode not installed - iOS tools unavailable. Install Xcode and run: xcode-select --install`;
|
|
492
532
|
const did = (n) => { try { return iosDevice(n); } catch (e) { return null; } };
|
|
493
533
|
|
|
@@ -789,7 +829,6 @@ async function handleIOS(name, args, ctx = {}) {
|
|
|
789
829
|
const res = await listProviders(auth, ctx.signal);
|
|
790
830
|
return JSON.stringify({ authTier: auth.tier, authMethod: auth.method, ...res }, null, 2);
|
|
791
831
|
}
|
|
792
|
-
if (!args.ipa_path) return "ERROR: ipa_path is required (or pass list_providers=true)";
|
|
793
832
|
// Same finally rationale as ios_export_ipa: never leak the heartbeat.
|
|
794
833
|
const stopHeartbeat = startHeartbeat(ctx, "altool --validate-app");
|
|
795
834
|
let res;
|
|
@@ -822,8 +861,6 @@ async function handleIOS(name, args, ctx = {}) {
|
|
|
822
861
|
}
|
|
823
862
|
case "ios_xcodebuild": {
|
|
824
863
|
if (!HAS_XCRUN) return "ERROR: xcrun not available - Xcode Command Line Tools required";
|
|
825
|
-
if (!args.project && !args.workspace) return "ERROR: project or workspace required";
|
|
826
|
-
if (args.project && args.workspace) return "ERROR: pass project OR workspace, not both";
|
|
827
864
|
const action = args.action || "build";
|
|
828
865
|
const config = args.configuration || "Release";
|
|
829
866
|
let dest = args.destination;
|
|
@@ -843,9 +880,13 @@ async function handleIOS(name, args, ctx = {}) {
|
|
|
843
880
|
const derivedFlag = args.derived_data_path ? `-derivedDataPath ${shq(args.derived_data_path)}` : "";
|
|
844
881
|
const actionMap = { "clean-build": "clean build", build: "build", test: "test", clean: "clean", archive: "archive" };
|
|
845
882
|
// extra_args is appended verbatim (multiple flags, so it can't be a single
|
|
846
|
-
// shq'd token), which makes it a shell passthrough.
|
|
847
|
-
//
|
|
848
|
-
//
|
|
883
|
+
// shq'd token), which makes it a shell passthrough. The same character class
|
|
884
|
+
// is declared as `pattern` on the tool schema, so the CallTool boundary
|
|
885
|
+
// refuses the payload before dispatch; this is the second line, kept because
|
|
886
|
+
// the string is one concatenation away from a shell. It lived only here
|
|
887
|
+
// until the Linux CI run showed why that was not enough: handleIOS returns
|
|
888
|
+
// "Xcode not installed" first, so on any host without Xcode the guard was
|
|
889
|
+
// never reached and the test that proves it works could not run.
|
|
849
890
|
const extra = args.extra_args || "";
|
|
850
891
|
if (/[;&|`$(){}<>\n\\]/.test(extra)) {
|
|
851
892
|
return "ERROR: extra_args may contain only plain flags and values (no shell metacharacters ; & | ` $ ( ) { } < > \\).";
|
|
@@ -971,7 +1012,6 @@ async function handleIOS(name, args, ctx = {}) {
|
|
|
971
1012
|
const mode = args.mode || "snapshot";
|
|
972
1013
|
const graphOut = args.output_graph ? ` --outputGraph=${shq(args.output_graph)}` : "";
|
|
973
1014
|
if (mode === "diff") {
|
|
974
|
-
if (!args.baseline_graph) return "ERROR: mode=diff needs baseline_graph, the memory graph saved by an earlier call";
|
|
975
1015
|
if (!existsSync(args.baseline_graph)) return `ERROR: baseline graph not found at ${args.baseline_graph}`;
|
|
976
1016
|
}
|
|
977
1017
|
const diffArg = mode === "diff" ? ` --diffFrom=${shq(args.baseline_graph)}` : "";
|
|
@@ -998,7 +1038,6 @@ async function handleIOS(name, args, ctx = {}) {
|
|
|
998
1038
|
}, null, 2);
|
|
999
1039
|
}
|
|
1000
1040
|
case "ios_accessibility_audit_deep": {
|
|
1001
|
-
if (!args.project && !args.workspace) return "ERROR: pass project or workspace";
|
|
1002
1041
|
const container = args.workspace ? `-workspace ${shq(args.workspace)}` : `-project ${shq(args.project)}`;
|
|
1003
1042
|
const d = iosDevice(args.device_id);
|
|
1004
1043
|
const bundle = join(SCREENSHOT_DIR, `a11ydeep_${Date.now()}.xcresult`);
|
|
@@ -1345,17 +1384,29 @@ async function handleAndroid(name, args, ctx = {}) {
|
|
|
1345
1384
|
}
|
|
1346
1385
|
// 2. Signing check. apksigner exits 1 on a failed verification with the
|
|
1347
1386
|
// verdict on stdout, so the output is captured whatever the status.
|
|
1387
|
+
//
|
|
1388
|
+
// "signed" needs positive evidence - apksigner's own `Verifies` line or a
|
|
1389
|
+
// printed certificate. The branch used to be the else of three negative
|
|
1390
|
+
// tests, so ANY output it did not recognise became "APK is signed": on a
|
|
1391
|
+
// host with apksigner installed, 1KB of zero bytes audited as a correctly
|
|
1392
|
+
// signed APK. It read as correct on a Mac only because apksigner is not
|
|
1393
|
+
// there, and "command not found" is one of the strings it did recognise.
|
|
1348
1394
|
const signingInfo = runCapture(`apksigner verify --print-certs ${shq(p)} 2>&1`);
|
|
1395
|
+
const verifies = /^Verifies\b/m.test(signingInfo) || /Signer #\d+ certificate DN:/.test(signingInfo);
|
|
1349
1396
|
if (/DOES NOT VERIFY/.test(signingInfo)) {
|
|
1350
1397
|
findings.push({ check: "signing", status: "critical", detail: signingInfo.slice(0, 500) });
|
|
1351
1398
|
} else if (isFailure(signingInfo) || /command not found|No such file or directory/i.test(signingInfo)) {
|
|
1352
1399
|
findings.push({ check: "signing", status: "warning", detail: "apksigner not found - install Android SDK Build-Tools to verify the signature" });
|
|
1353
|
-
} else if (
|
|
1354
|
-
findings.push({ check: "signing", status: "warning", detail: signingInfo.slice(0, 500) });
|
|
1355
|
-
} else {
|
|
1400
|
+
} else if (verifies) {
|
|
1356
1401
|
const hasV2 = signingInfo.includes("v2 scheme") || (runOrNull(`apksigner verify -v ${shq(p)} 2>&1`) || "").includes("Verified using v2");
|
|
1357
1402
|
findings.push({ check: "signing", status: "pass", detail: "APK is signed" });
|
|
1358
1403
|
findings.push({ check: "signing_v2", status: hasV2 ? "pass" : "warning", detail: hasV2 ? "v2+ signature present" : "Only v1 signature - consider v2+ for tamper protection" });
|
|
1404
|
+
} else {
|
|
1405
|
+
findings.push({
|
|
1406
|
+
check: "signing",
|
|
1407
|
+
status: "warning",
|
|
1408
|
+
detail: `apksigner did not report a verified signature; treat this as unverified, not as signed: ${signingInfo.slice(0, 400) || "(no output)"}`,
|
|
1409
|
+
});
|
|
1359
1410
|
}
|
|
1360
1411
|
// 3. File size
|
|
1361
1412
|
try {
|
|
@@ -1574,6 +1625,8 @@ async function dispatchStep(tool, stepArgs) {
|
|
|
1574
1625
|
else if (tool.startsWith("android_")) result = await handleAndroid(tool, stepArgs);
|
|
1575
1626
|
else if (tool.startsWith("web_")) result = await handleWeb(tool, stepArgs);
|
|
1576
1627
|
else if (tool.startsWith("design_")) result = await handleDesign(tool, stepArgs, designCtx);
|
|
1628
|
+
else if (tool.startsWith("code_")) result = await handleCode(tool, stepArgs, {});
|
|
1629
|
+
else if (tool.startsWith("pass_")) result = await handlePass(tool, stepArgs);
|
|
1577
1630
|
else throw new Error(`Unknown tool: ${tool}`);
|
|
1578
1631
|
// Every handler's `default:` arm returns null, and that is its only source of
|
|
1579
1632
|
// null, so null means "I do not recognise this tool". The throw below used to
|
|
@@ -1652,7 +1705,15 @@ async function handleAgent(name, args) {
|
|
|
1652
1705
|
|
|
1653
1706
|
// ── Server ──
|
|
1654
1707
|
|
|
1655
|
-
const ALL_TOOLS = [
|
|
1708
|
+
const ALL_TOOLS = [
|
|
1709
|
+
...IOS_TOOLS,
|
|
1710
|
+
...ANDROID_TOOLS,
|
|
1711
|
+
...WEB_TOOLS,
|
|
1712
|
+
...AGENT_TOOLS,
|
|
1713
|
+
...DESIGN_TOOLS,
|
|
1714
|
+
...CODE_TOOLS,
|
|
1715
|
+
...PASS_TOOLS,
|
|
1716
|
+
];
|
|
1656
1717
|
|
|
1657
1718
|
// Name -> inputSchema, so the CallTool boundary can enforce the declared shape.
|
|
1658
1719
|
const TOOL_SCHEMAS = new Map(ALL_TOOLS.map((t) => [t.name, t.inputSchema || {}]));
|
|
@@ -1682,6 +1743,74 @@ function schemaTypeOk(value, type) {
|
|
|
1682
1743
|
}
|
|
1683
1744
|
}
|
|
1684
1745
|
|
|
1746
|
+
// Constraint keywords beyond type/required/enum. A schema that declares
|
|
1747
|
+
// `pattern` or `maximum` and has nobody enforcing it is worse than one that
|
|
1748
|
+
// declares nothing: the host shows the caller a contract the server does not
|
|
1749
|
+
// keep. Everything a tool literal is allowed to write is checked here.
|
|
1750
|
+
//
|
|
1751
|
+
// `patternHint` is ours, not JSON Schema. A regex in an error message tells a
|
|
1752
|
+
// human nothing; the hint says what the rule is in words. Hosts ignore unknown
|
|
1753
|
+
// keywords, so it rides along in inputSchema harmlessly.
|
|
1754
|
+
function checkConstraints(key, value, spec) {
|
|
1755
|
+
if (spec.type && !schemaTypeOk(value, spec.type)) {
|
|
1756
|
+
return `argument '${key}' must be ${spec.type}, got ${Array.isArray(value) ? "array" : typeof value}`;
|
|
1757
|
+
}
|
|
1758
|
+
if (Array.isArray(spec.enum) && !spec.enum.includes(value)) {
|
|
1759
|
+
return `argument '${key}' must be one of ${JSON.stringify(spec.enum)}`;
|
|
1760
|
+
}
|
|
1761
|
+
if (typeof spec.pattern === "string" && !new RegExp(spec.pattern).test(String(value))) {
|
|
1762
|
+
return spec.patternHint
|
|
1763
|
+
? `argument '${key}' ${spec.patternHint}`
|
|
1764
|
+
: `argument '${key}' must match ${spec.pattern}`;
|
|
1765
|
+
}
|
|
1766
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
1767
|
+
const str = String(value);
|
|
1768
|
+
if (Number.isFinite(spec.minLength) && str.length < spec.minLength) {
|
|
1769
|
+
return `argument '${key}' must be at least ${spec.minLength} characters`;
|
|
1770
|
+
}
|
|
1771
|
+
if (Number.isFinite(spec.maxLength) && str.length > spec.maxLength) {
|
|
1772
|
+
return `argument '${key}' must be at most ${spec.maxLength} characters`;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
if (spec.type === "number" || spec.type === "integer") {
|
|
1776
|
+
const num = Number(value);
|
|
1777
|
+
if (Number.isFinite(spec.minimum) && num < spec.minimum) {
|
|
1778
|
+
return `argument '${key}' must be >= ${spec.minimum}`;
|
|
1779
|
+
}
|
|
1780
|
+
if (Number.isFinite(spec.maximum) && num > spec.maximum) {
|
|
1781
|
+
return `argument '${key}' must be <= ${spec.maximum}`;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (Array.isArray(value)) {
|
|
1785
|
+
if (Number.isFinite(spec.minItems) && value.length < spec.minItems) {
|
|
1786
|
+
return `argument '${key}' must have at least ${spec.minItems} item(s)`;
|
|
1787
|
+
}
|
|
1788
|
+
if (Number.isFinite(spec.maxItems) && value.length > spec.maxItems) {
|
|
1789
|
+
return `argument '${key}' must have at most ${spec.maxItems} item(s)`;
|
|
1790
|
+
}
|
|
1791
|
+
if (spec.items) {
|
|
1792
|
+
for (let i = 0; i < value.length; i++) {
|
|
1793
|
+
const err = checkConstraints(`${key}[${i}]`, value[i], spec.items);
|
|
1794
|
+
if (err) return err;
|
|
1795
|
+
for (const req of spec.items.required || []) {
|
|
1796
|
+
const el = value[i];
|
|
1797
|
+
if (el === null || typeof el !== "object" || el[req] === undefined || el[req] === null) {
|
|
1798
|
+
return `argument '${key}[${i}]' is missing required field: ${req}`;
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
for (const [k, sub] of Object.entries(spec.items.properties || {})) {
|
|
1802
|
+
const el = value[i];
|
|
1803
|
+
if (el === null || typeof el !== "object") continue;
|
|
1804
|
+
if (el[k] === undefined || el[k] === null) continue;
|
|
1805
|
+
const subErr = checkConstraints(`${key}[${i}].${k}`, el[k], sub);
|
|
1806
|
+
if (subErr) return subErr;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return null;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1685
1814
|
function validateArgs(name, args) {
|
|
1686
1815
|
const schema = TOOL_SCHEMAS.get(name);
|
|
1687
1816
|
if (!schema || schema.type !== "object") return null;
|
|
@@ -1693,12 +1822,8 @@ function validateArgs(name, args) {
|
|
|
1693
1822
|
}
|
|
1694
1823
|
for (const [key, spec] of Object.entries(props)) {
|
|
1695
1824
|
if (args[key] === undefined || args[key] === null) continue;
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
}
|
|
1699
|
-
if (Array.isArray(spec.enum) && !spec.enum.includes(args[key])) {
|
|
1700
|
-
return `argument '${key}' must be one of ${JSON.stringify(spec.enum)}`;
|
|
1701
|
-
}
|
|
1825
|
+
const err = checkConstraints(key, args[key], spec);
|
|
1826
|
+
if (err) return err;
|
|
1702
1827
|
}
|
|
1703
1828
|
return null;
|
|
1704
1829
|
}
|
|
@@ -1716,6 +1841,11 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
1716
1841
|
"android_list_crashes",
|
|
1717
1842
|
"web_screenshot", "web_get_text",
|
|
1718
1843
|
"design_mock_detect", "design_scenario_inventory", "design_ui_geometry", "design_visual_compare",
|
|
1844
|
+
// Seven of the eight. Every code_* tool reads source and answers; the one
|
|
1845
|
+
// that is not here stops a cached server.
|
|
1846
|
+
...CODE_READ_ONLY,
|
|
1847
|
+
// pass_build writes a file the caller named; the other three only read.
|
|
1848
|
+
...PASS_READ_ONLY,
|
|
1719
1849
|
]);
|
|
1720
1850
|
|
|
1721
1851
|
// Irreversible on the target device: data loss the caller cannot undo.
|
|
@@ -1737,6 +1867,7 @@ const IDEMPOTENT_TOOLS = new Set([
|
|
|
1737
1867
|
"android_set_locale", "android_set_location", "android_grant_permission", "android_revoke_permission",
|
|
1738
1868
|
"android_set_orientation",
|
|
1739
1869
|
"web_close",
|
|
1870
|
+
...CODE_IDEMPOTENT,
|
|
1740
1871
|
]);
|
|
1741
1872
|
|
|
1742
1873
|
// Web tools reach arbitrary sites; everything else talks to a local simulator,
|
|
@@ -1840,6 +1971,10 @@ const bundleAuditSchema = (pathKey) => ({
|
|
|
1840
1971
|
});
|
|
1841
1972
|
|
|
1842
1973
|
const OUTPUT_SCHEMAS = {
|
|
1974
|
+
// Every code_* root is an object. A `{type:"array"}` root on ONE tool once
|
|
1975
|
+
// made all 78 unavailable in Claude Code - see the note further down.
|
|
1976
|
+
...CODE_OUTPUT_SCHEMAS,
|
|
1977
|
+
...PASS_OUTPUT_SCHEMAS,
|
|
1843
1978
|
ios_accessibility_audit: ACCESSIBILITY_AUDIT_SCHEMA,
|
|
1844
1979
|
android_accessibility_audit: ACCESSIBILITY_AUDIT_SCHEMA,
|
|
1845
1980
|
|
|
@@ -2037,6 +2172,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
2037
2172
|
else if (name.startsWith("web_")) result = await handleWeb(name, args || {});
|
|
2038
2173
|
else if (name.startsWith("agent_")) result = await handleAgent(name, args || {});
|
|
2039
2174
|
else if (name.startsWith("design_")) result = await handleDesign(name, args || {}, designCtx);
|
|
2175
|
+
// ...ctx, unlike the design_ line above: code-intel needs `signal` so an
|
|
2176
|
+
// aborted call cancels the in-flight LSP request instead of orphaning it.
|
|
2177
|
+
else if (name.startsWith("code_")) result = await handleCode(name, args || {}, ctx);
|
|
2178
|
+
else if (name.startsWith("pass_")) result = await handlePass(name, args || {});
|
|
2040
2179
|
else return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
2041
2180
|
// Same reason as dispatchStep: a handler returns null only from its
|
|
2042
2181
|
// `default:` arm, so an unrecognised name that happens to carry a known
|
|
@@ -2107,8 +2246,8 @@ function stopRecorders() {
|
|
|
2107
2246
|
}
|
|
2108
2247
|
}
|
|
2109
2248
|
|
|
2110
|
-
process.on("SIGTERM", async () => { stopRecorders(); await closeBrowser(); process.exit(0); });
|
|
2111
|
-
process.on("SIGINT", async () => { stopRecorders(); await closeBrowser(); process.exit(0); });
|
|
2249
|
+
process.on("SIGTERM", async () => { stopRecorders(); shutdownAllLsp(); await closeBrowser(); process.exit(0); });
|
|
2250
|
+
process.on("SIGINT", async () => { stopRecorders(); shutdownAllLsp(); await closeBrowser(); process.exit(0); });
|
|
2112
2251
|
|
|
2113
2252
|
const transport = new StdioServerTransport();
|
|
2114
2253
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmerterden/multi-agent-toolkit-mcp",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "MCP server for iOS Simulator, Android Emulator and headless web control.
|
|
3
|
+
"version": "3.12.0",
|
|
4
|
+
"description": "MCP server for iOS Simulator, Android Emulator and headless web control. 99 tools: device automation (tap/swipe/type), accessibility audits, visual diff, crash logs, App Store / Play Store pre-submission compliance. Runs standalone over stdio with any MCP client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"start": "node index.js",
|
|
13
|
-
"test": "node --test tools/design-check/__tests__/design-check.test.mjs tools/design-check/__tests__/plan-determinism.test.mjs tools/ios-app-store-audit/__tests__/app-store-audit.test.mjs tools/ios-testflight/__tests__/testflight.test.mjs tools/ui-inspect/__tests__/ui-inspect.test.mjs tools/crash-logs/__tests__/crash-logs.test.mjs tools/a11y/__tests__/a11y.test.mjs tools/launch-time/__tests__/launch-time.test.mjs tools/memory/__tests__/memory.test.mjs tools/offload/__tests__/offload.test.mjs __tests__/server-tools.test.mjs __tests__/injection.test.mjs",
|
|
13
|
+
"test": "node --test tools/design-check/__tests__/design-check.test.mjs tools/design-check/__tests__/plan-determinism.test.mjs tools/ios-app-store-audit/__tests__/app-store-audit.test.mjs tools/ios-testflight/__tests__/testflight.test.mjs tools/ui-inspect/__tests__/ui-inspect.test.mjs tools/code-intel/__tests__/code-intel.test.mjs tools/pass-kit/__tests__/pass-kit.test.mjs tools/crash-logs/__tests__/crash-logs.test.mjs tools/a11y/__tests__/a11y.test.mjs tools/launch-time/__tests__/launch-time.test.mjs tools/memory/__tests__/memory.test.mjs tools/offload/__tests__/offload.test.mjs __tests__/server-tools.test.mjs __tests__/injection.test.mjs",
|
|
14
14
|
"gates": "bash scripts/gates.sh"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
@@ -73,6 +73,12 @@
|
|
|
73
73
|
"CHANGELOG.md",
|
|
74
74
|
"LICENSE"
|
|
75
75
|
],
|
|
76
|
+
"_overridesReadme": "Three advisories reach this tree only through @modelcontextprotocol/sdk, and all three are fixed inside the major their consumer already depends on. The upper bound on fast-uri is load-bearing: ajv@8 declares ^3, and an unbounded >=3.1.6 resolves to 4.x, which is a different major under a dependency that never asked for it.",
|
|
77
|
+
"overrides": {
|
|
78
|
+
"fast-uri": ">=3.1.6 <4",
|
|
79
|
+
"hono": ">=4.13.5 <5",
|
|
80
|
+
"qs": ">=6.16.0 <7"
|
|
81
|
+
},
|
|
76
82
|
"dependencies": {
|
|
77
83
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
78
84
|
"pixelmatch": "^7.2.0",
|