@algosuite/vo-mcp 0.2.0-beta.8 → 0.2.0-beta.9
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 +26 -2
- package/bin/vo-mcp +1 -0
- package/dist/cli.js +159 -0
- package/dist/cli.js.map +3 -3
- package/dist/index.js +158 -0
- package/dist/index.js.map +3 -3
- package/dist/install-cli.js +196 -54
- package/dist/install-cli.js.map +4 -4
- package/dist/runner-cli.js +24 -24
- package/dist/runner-cli.js.map +3 -3
- package/dist/runner-supervisor.js +71 -122
- package/dist/runner-supervisor.js.map +4 -4
- package/dist/supervisor-credential-helper.js +125 -0
- package/dist/supervisor-credential-helper.js.map +7 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,10 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
Virtual Office MCP server — the open protocol surface that exposes VO's consensus and ratchet tool family to any MCP-capable LLM client (Claude Code, Claude Desktop, Cursor, Continue, Codex, etc.).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The live AlgoHQ whiteboard is available to every MCP client through
|
|
6
|
+
`hq_whiteboard_post` and `hq_whiteboard_read`. Both use the scoped credential
|
|
7
|
+
created by `vo-mcp login`; tenant/operator ownership is derived by the control
|
|
8
|
+
plane and cannot be widened by tool input. The control plane admits scoped
|
|
9
|
+
credentials only for operator IDs on its internal `HQ_WHITEBOARD_OPERATOR_IDS`
|
|
10
|
+
allowlist; arbitrary self-serve tenants cannot read or write the shared fleet board.
|
|
11
|
+
|
|
12
|
+
**Status:** Phase 2. Stdio transport. **26 tools registered** (source of truth: `src/server.ts` `buildToolRegistry()`). The static-ratchet tools (`vo_check_assertion_strength`, `vo_check_ratchets`) run locally today; the consensus-routed tools fall back to `{ verdict: "unimplemented", ... }` until engine credentials are present; the heal-family, PR-admin, session-state, and AlgoHQ whiteboard tools forward to the control plane in cloud mode. `vo_review_merge` is a **read-only consensus pre-merge review** (the verify-before-act gate for the Command Center — never merges). The shape is stable; the institutional-knowledge content is loaded from closed packages.
|
|
6
13
|
|
|
7
14
|
This package is intentionally **shell-only**. Per `docs/handoffs/vo-mcp-server-2026-05-21.md` §C-1: tool *definitions* (schemas, names, descriptions) ship openly. Tool *implementations* that encode institutional knowledge (specific ratchet thresholds, consensus prompt content, architectural-defaults knowledge base) live behind the cloud or in closed companion packages.
|
|
8
15
|
|
|
16
|
+
## Install and refresh client configuration
|
|
17
|
+
|
|
18
|
+
`vo-mcp install` safely registers the same required `algohq` MCP server in
|
|
19
|
+
Claude Desktop, Claude Code, and Codex (`~/.codex/config.toml`), then offers
|
|
20
|
+
pairing and runner auto-start. Existing client settings and comments are
|
|
21
|
+
preserved, and every changed config is backed up first.
|
|
22
|
+
|
|
23
|
+
To refresh client configuration on an already-paired runner without touching
|
|
24
|
+
its keychain credential or auto-start service, use the noninteractive form:
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
vo-mcp install --config-only
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Restart the MCP clients and runner after updating, then use
|
|
31
|
+
[AlgoHQ](https://algosuite.ai/algohq) to dispatch work.
|
|
32
|
+
|
|
9
33
|
## Tools
|
|
10
34
|
|
|
11
35
|
| Name | Phase 1 status | Description |
|
|
@@ -44,7 +68,7 @@ pnpm test # vitest run
|
|
|
44
68
|
|
|
45
69
|
The integration test (`test/integration/stdio-roundtrip.test.ts`) spawns the built CLI, exchanges JSON-RPC over stdio, and asserts:
|
|
46
70
|
|
|
47
|
-
- `initialize` + `tools/list` returns the
|
|
71
|
+
- `initialize` + `tools/list` returns the 26 expected tools.
|
|
48
72
|
- `vo_check_assertion_strength` runs end-to-end and writes a JSONL event line.
|
|
49
73
|
- A repeat call with identical input hits the cache (`cache.hit = true`).
|
|
50
74
|
- `vo_consensus_judgment` returns `unimplemented` and still writes its event line.
|
package/bin/vo-mcp
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Usage:
|
|
6
6
|
* vo-mcp # MCP stdio server (default)
|
|
7
7
|
* vo-mcp install # one-command installer
|
|
8
|
+
* vo-mcp install --config-only # refresh Claude + Codex MCP config only
|
|
8
9
|
* vo-mcp update # update MCP + runner package
|
|
9
10
|
* vo-mcp login # credential login (browser loopback)
|
|
10
11
|
* vo-mcp pair # device-code pairing (enter a code in the web)
|
package/dist/cli.js
CHANGED
|
@@ -5592,6 +5592,149 @@ async function handlePrivateKnowledgeContext(_deps, rawInput, _signal, fetchFn =
|
|
|
5592
5592
|
return jsonContent({ tool: CONTEXT_TOOL_NAME, schema_version: 1, payload });
|
|
5593
5593
|
}
|
|
5594
5594
|
|
|
5595
|
+
// src/tools/hq/whiteboard.ts
|
|
5596
|
+
init_auth_token_source();
|
|
5597
|
+
init_credential_store();
|
|
5598
|
+
init_common();
|
|
5599
|
+
var POST_TOOL_NAME = "hq_whiteboard_post";
|
|
5600
|
+
var READ_TOOL_NAME = "hq_whiteboard_read";
|
|
5601
|
+
var postDescription = "Post an append-only coordination note to the live AlgoHQ whiteboard. Uses the scoped credential from vo-mcp login; operator and tenant ownership are derived by the server.";
|
|
5602
|
+
var readDescription = "Read recent coordination notes from the caller's live AlgoHQ whiteboard. Uses the scoped credential from vo-mcp login and cannot widen tenant scope.";
|
|
5603
|
+
var postInputSchema = {
|
|
5604
|
+
type: "object",
|
|
5605
|
+
properties: {
|
|
5606
|
+
from: { type: "string", minLength: 1, maxLength: 100, description: "Agent/session display name." },
|
|
5607
|
+
type: { type: "string", minLength: 1, maxLength: 64, description: "Message kind, such as intent, worklog, blocker, or completion." },
|
|
5608
|
+
content: { type: "string", minLength: 1, maxLength: 500, description: "Short coordination note." },
|
|
5609
|
+
targetAgent: { type: "string", maxLength: 100 },
|
|
5610
|
+
tester: { type: "string", maxLength: 100 },
|
|
5611
|
+
tier: { type: "string", maxLength: 32 }
|
|
5612
|
+
},
|
|
5613
|
+
required: ["from", "type", "content"],
|
|
5614
|
+
additionalProperties: false
|
|
5615
|
+
};
|
|
5616
|
+
var readInputSchema = {
|
|
5617
|
+
type: "object",
|
|
5618
|
+
properties: {
|
|
5619
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 25 },
|
|
5620
|
+
since: { type: "string", description: "Optional ISO-8601 lower bound." },
|
|
5621
|
+
type: { type: "string", minLength: 1, maxLength: 64 }
|
|
5622
|
+
},
|
|
5623
|
+
additionalProperties: false
|
|
5624
|
+
};
|
|
5625
|
+
function resolveTimeoutMs() {
|
|
5626
|
+
const parsed = Number(process.env["HQ_WHITEBOARD_TIMEOUT_MS"]);
|
|
5627
|
+
return Number.isFinite(parsed) && parsed >= 10 && parsed <= 12e4 ? parsed : 1e4;
|
|
5628
|
+
}
|
|
5629
|
+
function isRecord(value) {
|
|
5630
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5631
|
+
}
|
|
5632
|
+
function onlyKeys(value, allowed) {
|
|
5633
|
+
return Object.keys(value).every((key) => allowed.includes(key));
|
|
5634
|
+
}
|
|
5635
|
+
function isBoundedString(value, min, max) {
|
|
5636
|
+
return typeof value === "string" && value.trim().length >= min && value.trim().length <= max;
|
|
5637
|
+
}
|
|
5638
|
+
function parsePostInput(value) {
|
|
5639
|
+
if (!isRecord(value) || !onlyKeys(value, ["from", "type", "content", "targetAgent", "tester", "tier"])) return null;
|
|
5640
|
+
if (!isBoundedString(value["from"], 1, 100)) return null;
|
|
5641
|
+
if (!isBoundedString(value["type"], 1, 64) || !/^[a-zA-Z0-9_-]+$/.test(value["type"].trim())) return null;
|
|
5642
|
+
if (!isBoundedString(value["content"], 1, 500)) return null;
|
|
5643
|
+
for (const [key, max] of [["targetAgent", 100], ["tester", 100], ["tier", 32]]) {
|
|
5644
|
+
if (value[key] !== void 0 && !isBoundedString(value[key], 0, max)) return null;
|
|
5645
|
+
}
|
|
5646
|
+
return {
|
|
5647
|
+
from: value["from"].trim(),
|
|
5648
|
+
type: value["type"].trim(),
|
|
5649
|
+
content: value["content"].trim(),
|
|
5650
|
+
...typeof value["targetAgent"] === "string" ? { targetAgent: value["targetAgent"].trim() } : {},
|
|
5651
|
+
...typeof value["tester"] === "string" ? { tester: value["tester"].trim() } : {},
|
|
5652
|
+
...typeof value["tier"] === "string" ? { tier: value["tier"].trim() } : {}
|
|
5653
|
+
};
|
|
5654
|
+
}
|
|
5655
|
+
function parseReadInput(value) {
|
|
5656
|
+
if (!isRecord(value) || !onlyKeys(value, ["limit", "since", "type"])) return null;
|
|
5657
|
+
if (value["limit"] !== void 0 && (!Number.isInteger(value["limit"]) || Number(value["limit"]) < 1 || Number(value["limit"]) > 100)) return null;
|
|
5658
|
+
if (value["since"] !== void 0 && (typeof value["since"] !== "string" || Number.isNaN(Date.parse(value["since"])))) return null;
|
|
5659
|
+
if (value["type"] !== void 0 && !isBoundedString(value["type"], 1, 64)) return null;
|
|
5660
|
+
return {
|
|
5661
|
+
...typeof value["limit"] === "number" ? { limit: value["limit"] } : {},
|
|
5662
|
+
...typeof value["since"] === "string" ? { since: value["since"] } : {},
|
|
5663
|
+
...typeof value["type"] === "string" ? { type: value["type"].trim() } : {}
|
|
5664
|
+
};
|
|
5665
|
+
}
|
|
5666
|
+
async function resolveCloud(fetchFn) {
|
|
5667
|
+
const url = process.env["VO_CONTROL_PLANE_URL"]?.trim().replace(/\/$/, "");
|
|
5668
|
+
if (!url) return null;
|
|
5669
|
+
try {
|
|
5670
|
+
const source = createAuthTokenSourceFromEnv(process.env, fetchFn, () => readStoredCredential(process.env));
|
|
5671
|
+
const token = await source?.getToken();
|
|
5672
|
+
return token ? { url, token } : null;
|
|
5673
|
+
} catch {
|
|
5674
|
+
return null;
|
|
5675
|
+
}
|
|
5676
|
+
}
|
|
5677
|
+
async function callWhiteboard(method, bodyOrQuery, signal, fetchFn = fetch) {
|
|
5678
|
+
const cloud = await resolveCloud(fetchFn);
|
|
5679
|
+
if (!cloud) {
|
|
5680
|
+
return {
|
|
5681
|
+
ok: false,
|
|
5682
|
+
error: "hq_whiteboard_not_configured",
|
|
5683
|
+
message: "Set VO_CONTROL_PLANE_URL and run vo-mcp login to install a scoped HQ credential."
|
|
5684
|
+
};
|
|
5685
|
+
}
|
|
5686
|
+
const timeoutSignal = AbortSignal.timeout(resolveTimeoutMs());
|
|
5687
|
+
const requestSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
5688
|
+
const query = new URLSearchParams();
|
|
5689
|
+
if (method === "GET") {
|
|
5690
|
+
const input = bodyOrQuery;
|
|
5691
|
+
query.set("limit", String(input.limit ?? 25));
|
|
5692
|
+
if (input.since) query.set("since", input.since);
|
|
5693
|
+
if (input.type) query.set("type", input.type);
|
|
5694
|
+
}
|
|
5695
|
+
try {
|
|
5696
|
+
const response = await fetchFn(
|
|
5697
|
+
`${cloud.url}/api/v1/hq/whiteboard/messages${query.size ? `?${query}` : ""}`,
|
|
5698
|
+
{
|
|
5699
|
+
method,
|
|
5700
|
+
headers: {
|
|
5701
|
+
Authorization: `Bearer ${cloud.token}`,
|
|
5702
|
+
...method === "POST" ? { "Content-Type": "application/json" } : {}
|
|
5703
|
+
},
|
|
5704
|
+
...method === "POST" ? { body: JSON.stringify(bodyOrQuery) } : {},
|
|
5705
|
+
signal: requestSignal
|
|
5706
|
+
}
|
|
5707
|
+
);
|
|
5708
|
+
const text = await response.text();
|
|
5709
|
+
let payload;
|
|
5710
|
+
try {
|
|
5711
|
+
payload = JSON.parse(text);
|
|
5712
|
+
} catch {
|
|
5713
|
+
payload = { ok: false, error: "invalid_response", message: text.slice(0, 200) };
|
|
5714
|
+
}
|
|
5715
|
+
if (!response.ok) {
|
|
5716
|
+
return { ok: false, error: "hq_whiteboard_http_error", status: response.status, response: payload };
|
|
5717
|
+
}
|
|
5718
|
+
return payload;
|
|
5719
|
+
} catch (error) {
|
|
5720
|
+
return {
|
|
5721
|
+
ok: false,
|
|
5722
|
+
error: signal?.aborted ? "cancelled" : timeoutSignal.aborted ? "hq_whiteboard_timeout" : "hq_whiteboard_unreachable",
|
|
5723
|
+
message: error instanceof Error ? error.message.slice(0, 200) : String(error).slice(0, 200)
|
|
5724
|
+
};
|
|
5725
|
+
}
|
|
5726
|
+
}
|
|
5727
|
+
async function handleHqWhiteboardPost(_deps, rawInput, signal) {
|
|
5728
|
+
const input = parsePostInput(rawInput);
|
|
5729
|
+
if (!input) throw invalidParams(POST_TOOL_NAME, "requires from, type, and 1-500 character content; unknown fields are rejected");
|
|
5730
|
+
return jsonContent(await callWhiteboard("POST", input, signal));
|
|
5731
|
+
}
|
|
5732
|
+
async function handleHqWhiteboardRead(_deps, rawInput, signal) {
|
|
5733
|
+
const input = parseReadInput(rawInput);
|
|
5734
|
+
if (!input) throw invalidParams(READ_TOOL_NAME, "limit must be 1-100, since must be ISO-8601, and unknown fields are rejected");
|
|
5735
|
+
return jsonContent(await callWhiteboard("GET", input, signal));
|
|
5736
|
+
}
|
|
5737
|
+
|
|
5595
5738
|
// src/server.ts
|
|
5596
5739
|
function buildToolRegistry() {
|
|
5597
5740
|
return {
|
|
@@ -5786,6 +5929,22 @@ function buildToolRegistry() {
|
|
|
5786
5929
|
inputSchema: contextInputSchema
|
|
5787
5930
|
},
|
|
5788
5931
|
handler: handlePrivateKnowledgeContext
|
|
5932
|
+
},
|
|
5933
|
+
[POST_TOOL_NAME]: {
|
|
5934
|
+
definition: {
|
|
5935
|
+
name: POST_TOOL_NAME,
|
|
5936
|
+
description: postDescription,
|
|
5937
|
+
inputSchema: postInputSchema
|
|
5938
|
+
},
|
|
5939
|
+
handler: handleHqWhiteboardPost
|
|
5940
|
+
},
|
|
5941
|
+
[READ_TOOL_NAME]: {
|
|
5942
|
+
definition: {
|
|
5943
|
+
name: READ_TOOL_NAME,
|
|
5944
|
+
description: readDescription,
|
|
5945
|
+
inputSchema: readInputSchema
|
|
5946
|
+
},
|
|
5947
|
+
handler: handleHqWhiteboardRead
|
|
5789
5948
|
}
|
|
5790
5949
|
};
|
|
5791
5950
|
}
|