@projectsolo/solo-mission-mcp 0.19.2 → 0.19.3
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/.env.example
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
SOLO_AGENT_KEY=your-agent-api-key-here
|
|
2
|
-
SOLO_MISSION_API_URL=https://api.mission.projectsolo.
|
|
2
|
+
SOLO_MISSION_API_URL=https://api.mission.projectsolo.ai
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
name: Release
|
|
2
2
|
|
|
3
|
+
# Tag prefix picks the runner: v1.2.3 runs on GitHub-hosted ubuntu-latest as
|
|
4
|
+
# before; u1.2.3 runs the identical job on our self-hosted VPS runner
|
|
5
|
+
# instead (registered with labels self-hosted,solo_mission_mcp-vps), to
|
|
6
|
+
# avoid burning GitHub-hosted runner minutes. Same steps, same OIDC identity
|
|
7
|
+
# for npm Trusted Publishing either way — the OIDC token is issued by
|
|
8
|
+
# GitHub's Actions service to the job, not tied to which runner executes it.
|
|
3
9
|
on:
|
|
4
10
|
push:
|
|
5
11
|
tags:
|
|
6
12
|
- 'v[0-9]+.[0-9]+.[0-9]+'
|
|
13
|
+
- 'u[0-9]+.[0-9]+.[0-9]+'
|
|
7
14
|
|
|
8
15
|
jobs:
|
|
9
16
|
publish:
|
|
10
17
|
name: Build & Publish to npm
|
|
11
|
-
runs-on: ubuntu-latest
|
|
18
|
+
runs-on: ${{ startsWith(github.ref_name, 'u') && fromJSON('["self-hosted","solo_mission_mcp-vps"]') || 'ubuntu-latest' }}
|
|
12
19
|
permissions:
|
|
13
20
|
contents: read
|
|
14
21
|
id-token: write
|
|
@@ -37,12 +44,15 @@ jobs:
|
|
|
37
44
|
- name: Build
|
|
38
45
|
run: npm run build
|
|
39
46
|
|
|
47
|
+
- name: Check tool coverage against live API spec
|
|
48
|
+
run: npx tsx src/scripts/check-tools-against-spec.ts
|
|
49
|
+
|
|
40
50
|
- name: Verify tag matches package.json version
|
|
41
51
|
run: |
|
|
42
|
-
TAG_VERSION="${GITHUB_REF_NAME#
|
|
52
|
+
TAG_VERSION="${GITHUB_REF_NAME#[uv]}"
|
|
43
53
|
PKG_VERSION="$(node -p "require('./package.json').version")"
|
|
44
54
|
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
|
|
45
|
-
echo "Tag
|
|
55
|
+
echo "Tag $GITHUB_REF_NAME does not match package.json version $PKG_VERSION"
|
|
46
56
|
exit 1
|
|
47
57
|
fi
|
|
48
58
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Release gate: fails if solo-firebase's live OpenAPI spec documents a route that no
|
|
4
|
+
* MCP tool in this package actually calls.
|
|
5
|
+
*
|
|
6
|
+
* Companion to solo_firebase#196 (mission-api now serves GET /agent/openapi.json,
|
|
7
|
+
* generated live from its own route annotations) and solo_mission_web#156 (the same
|
|
8
|
+
* gate for the /developers page). Without something on this side checking against it,
|
|
9
|
+
* a live spec existing doesn't stop these tools from drifting the same way the docs
|
|
10
|
+
* page did in solo_mission_web#154.
|
|
11
|
+
*
|
|
12
|
+
* Direction of the check, and why: this package's tool set is intentionally BIGGER
|
|
13
|
+
* than what the spec currently covers (36 tools vs. 19 annotated operations — most of
|
|
14
|
+
* solo-firebase's mission-api routes aren't annotated yet, see solo_firebase#196's
|
|
15
|
+
* "next steps"). Checking "every REST call in this package must appear in the spec"
|
|
16
|
+
* would fail on every one of those un-annotated routes for no real reason. Checking
|
|
17
|
+
* the other direction — "every route the spec DOES document must be called by
|
|
18
|
+
* something here" — only walks the spec's small, well-defined set, so it stays
|
|
19
|
+
* meaningful without needing a maintained allowlist of which tools are in scope.
|
|
20
|
+
*
|
|
21
|
+
* Extraction is static regex over src/tools/*.ts (same approach as solo-firebase's own
|
|
22
|
+
* openapiSpecConformance.test.ts and solo-mission-web's check-dev-docs-against-spec.mjs
|
|
23
|
+
* — no runtime app boot, no live network call except the one spec fetch), matching
|
|
24
|
+
* apiGet/apiPost/apiPut/apiDelete/publicApiPost call sites, tolerating the generic type
|
|
25
|
+
* argument (apiPost<T>(...)) and multi-line calls actually used in this codebase.
|
|
26
|
+
* Template-literal interpolations (${args.mission_id}) and OpenAPI {id}-style path
|
|
27
|
+
* params are both normalized to a generic :param token and compared positionally —
|
|
28
|
+
* exact param names don't need to match, only path shape.
|
|
29
|
+
*
|
|
30
|
+
* Usage: npx tsx src/scripts/check-tools-against-spec.ts
|
|
31
|
+
* Override the spec URL (e.g. against a local/staging backend) with SPEC_URL=...
|
|
32
|
+
*/
|
|
33
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
34
|
+
import { fileURLToPath } from 'node:url';
|
|
35
|
+
import { dirname, join } from 'node:path';
|
|
36
|
+
|
|
37
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const TOOLS_DIR = join(__dirname, '../tools');
|
|
39
|
+
const SPEC_URL = process.env.SPEC_URL ?? 'https://api.mission.projectsolo.ai/agent/openapi.json';
|
|
40
|
+
|
|
41
|
+
const METHOD_BY_FN: Record<string, string> = {
|
|
42
|
+
apiGet: 'GET',
|
|
43
|
+
apiPost: 'POST',
|
|
44
|
+
apiPut: 'PUT',
|
|
45
|
+
apiDelete: 'DELETE',
|
|
46
|
+
publicApiPost: 'POST',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function normalizeParams(path: string): string {
|
|
50
|
+
return path
|
|
51
|
+
.split('?')[0] // strip query strings (e.g. the upload-url ?content_type=... calls)
|
|
52
|
+
.replace(/\$\{[^}]*\}/g, ':param') // template-literal interpolations
|
|
53
|
+
.replace(/\{[^}]*\}/g, ':param'); // OpenAPI {param} style, for symmetry if ever mixed in
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Every "METHOD normalizedPath" this package's tools actually call, from any tools/*.ts file. */
|
|
57
|
+
function extractCalledRoutes(): Set<string> {
|
|
58
|
+
const routes = new Set<string>();
|
|
59
|
+
const files = readdirSync(TOOLS_DIR).filter((f) => f.endsWith('.ts'));
|
|
60
|
+
const fnNames = Object.keys(METHOD_BY_FN).join('|');
|
|
61
|
+
// Matches both the common case — a string/template literal passed straight into the
|
|
62
|
+
// call — and an identifier (e.g. apiPost(path)) built from a `const path = \`...\``
|
|
63
|
+
// a line or two earlier, which conversations.ts's upload-url tools both do (the path
|
|
64
|
+
// there needs a query string appended, so it's assembled before the call).
|
|
65
|
+
const directCallPattern = new RegExp(`\\b(${fnNames})\\s*(?:<[^>]*>)?\\s*\\(\\s*(\`|'|")((?:(?!\\2).)*)\\2`, 'gs');
|
|
66
|
+
const indirectCallPattern = new RegExp(`\\b(${fnNames})\\s*(?:<[^>]*>)?\\s*\\(\\s*([A-Za-z_$][A-Za-z0-9_$]*)\\s*[,)]`, 'g');
|
|
67
|
+
const constAssignPattern = /const\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(`|'|")((?:(?!\2).)*)\2/gs;
|
|
68
|
+
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
const source = readFileSync(join(TOOLS_DIR, file), 'utf8');
|
|
71
|
+
|
|
72
|
+
let match: RegExpExecArray | null;
|
|
73
|
+
while ((match = directCallPattern.exec(source)) !== null) {
|
|
74
|
+
const [, fnName, , rawPath] = match;
|
|
75
|
+
routes.add(`${METHOD_BY_FN[fnName]} ${normalizeParams(rawPath)}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const localPaths = new Map<string, string>();
|
|
79
|
+
while ((match = constAssignPattern.exec(source)) !== null) {
|
|
80
|
+
const [, varName, , rawValue] = match;
|
|
81
|
+
if (rawValue.startsWith('/')) localPaths.set(varName, rawValue);
|
|
82
|
+
}
|
|
83
|
+
while ((match = indirectCallPattern.exec(source)) !== null) {
|
|
84
|
+
const [, fnName, varName] = match;
|
|
85
|
+
const rawPath = localPaths.get(varName);
|
|
86
|
+
if (rawPath) routes.add(`${METHOD_BY_FN[fnName]} ${normalizeParams(rawPath)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return routes;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Every "METHOD normalizedPath" the live spec documents. */
|
|
93
|
+
async function fetchSpecRoutes(): Promise<string[]> {
|
|
94
|
+
const res = await fetch(SPEC_URL);
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
throw new Error(`Failed to fetch ${SPEC_URL}: HTTP ${res.status}`);
|
|
97
|
+
}
|
|
98
|
+
const spec = (await res.json()) as { paths?: Record<string, Record<string, unknown>> };
|
|
99
|
+
const routes: string[] = [];
|
|
100
|
+
for (const [openApiPath, operations] of Object.entries(spec.paths ?? {})) {
|
|
101
|
+
const normalizedPath = normalizeParams(openApiPath);
|
|
102
|
+
for (const method of Object.keys(operations)) {
|
|
103
|
+
routes.push(`${method.toUpperCase()} ${normalizedPath}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return routes;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const called = extractCalledRoutes();
|
|
110
|
+
if (called.size < 10) {
|
|
111
|
+
// Sanity check on the extraction itself — if the call-site shape changes and the
|
|
112
|
+
// regex stops matching, fail loudly instead of silently passing on a near-empty set.
|
|
113
|
+
console.error(`Only extracted ${called.size} called routes from src/tools/*.ts — expected 15+. Regex may be out of sync with the call-site shape.`);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let specRoutes: string[];
|
|
118
|
+
try {
|
|
119
|
+
specRoutes = await fetchSpecRoutes();
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error(`Could not verify tool coverage against the live API spec: ${(err as Error).message}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const uncalled = specRoutes.filter((route) => !called.has(route) && !route.endsWith('/agent/openapi.json'));
|
|
126
|
+
|
|
127
|
+
if (uncalled.length > 0) {
|
|
128
|
+
console.error("The live API spec documents routes that no tool in src/tools/*.ts calls:");
|
|
129
|
+
for (const route of uncalled) console.error(` - ${route}`);
|
|
130
|
+
console.error(`\nSpec source: ${SPEC_URL}`);
|
|
131
|
+
console.error("Either a tool's REST call was changed/removed without updating this check's");
|
|
132
|
+
console.error('expectations, or the backend added something here that this package should');
|
|
133
|
+
console.error('expose a tool for.');
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(`OK: all ${specRoutes.length} spec-documented routes are called by at least one tool.`);
|