@useorgx/openclaw-plugin 0.4.5 → 0.4.6
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 +24 -3
- package/dashboard/dist/assets/0tOC3wSN.js +214 -0
- package/dashboard/dist/assets/B3ziCA02.js +8 -0
- package/dashboard/dist/assets/Bm8QnMJ_.js +1 -0
- package/dashboard/dist/assets/CpJsfbXo.js +9 -0
- package/dashboard/dist/assets/CyxZio4Y.js +1 -0
- package/dashboard/dist/assets/DaAIOik3.css +1 -0
- package/dashboard/dist/assets/sAhvFnpk.js +4 -0
- package/dashboard/dist/index.html +5 -5
- package/dist/activity-store.d.ts +28 -0
- package/dist/activity-store.js +250 -0
- package/dist/agent-context-store.d.ts +19 -0
- package/dist/agent-context-store.js +60 -3
- package/dist/agent-suite.d.ts +83 -0
- package/dist/agent-suite.js +615 -0
- package/dist/contracts/client.d.ts +22 -1
- package/dist/contracts/client.js +120 -3
- package/dist/contracts/types.d.ts +190 -1
- package/dist/entity-comment-store.d.ts +29 -0
- package/dist/entity-comment-store.js +190 -0
- package/dist/hooks/post-reporting-event.mjs +326 -0
- package/dist/http-handler.d.ts +7 -1
- package/dist/http-handler.js +3603 -578
- package/dist/index.js +936 -62
- package/dist/mcp-client-setup.js +156 -24
- package/dist/mcp-http-handler.d.ts +17 -0
- package/dist/mcp-http-handler.js +144 -3
- package/dist/next-up-queue-store.d.ts +31 -0
- package/dist/next-up-queue-store.js +169 -0
- package/dist/openclaw.plugin.json +1 -1
- package/dist/outbox.d.ts +1 -1
- package/dist/runtime-instance-store.d.ts +1 -1
- package/dist/runtime-instance-store.js +20 -3
- package/dist/skill-pack-state.d.ts +69 -0
- package/dist/skill-pack-state.js +232 -0
- package/dist/worker-supervisor.d.ts +25 -0
- package/dist/worker-supervisor.js +62 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +10 -1
- package/skills/orgx-design-agent/SKILL.md +38 -0
- package/skills/orgx-engineering-agent/SKILL.md +55 -0
- package/skills/orgx-marketing-agent/SKILL.md +40 -0
- package/skills/orgx-operations-agent/SKILL.md +40 -0
- package/skills/orgx-orchestrator-agent/SKILL.md +45 -0
- package/skills/orgx-product-agent/SKILL.md +39 -0
- package/skills/orgx-sales-agent/SKILL.md +40 -0
- package/skills/ship/SKILL.md +63 -0
- package/dashboard/dist/assets/B68j2crt.js +0 -1
- package/dashboard/dist/assets/BZZ-fiJx.js +0 -32
- package/dashboard/dist/assets/BoXlCHKa.js +0 -9
- package/dashboard/dist/assets/Bq9x_Xyh.css +0 -1
- package/dashboard/dist/assets/DBhrRVdp.js +0 -1
- package/dashboard/dist/assets/DD1jv1Hd.js +0 -8
- package/dashboard/dist/assets/DNjbmawF.js +0 -214
package/dist/mcp-client-setup.js
CHANGED
|
@@ -3,6 +3,27 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { writeFileAtomicSync, writeJsonFileAtomicSync } from "./fs-utils.js";
|
|
6
|
+
const ORGX_LOCAL_MCP_KEY = "orgx-openclaw";
|
|
7
|
+
const ORGX_HOSTED_MCP_URL = "https://mcp.useorgx.com/mcp";
|
|
8
|
+
const ORGX_LOCAL_MCP_SCOPES = [
|
|
9
|
+
"engineering",
|
|
10
|
+
"product",
|
|
11
|
+
"design",
|
|
12
|
+
"marketing",
|
|
13
|
+
"sales",
|
|
14
|
+
"operations",
|
|
15
|
+
"orchestration",
|
|
16
|
+
];
|
|
17
|
+
function scopedMcpServerKey(scope) {
|
|
18
|
+
return `${ORGX_LOCAL_MCP_KEY}-${scope}`;
|
|
19
|
+
}
|
|
20
|
+
function scopedMcpUrl(localMcpUrl, scope) {
|
|
21
|
+
const base = localMcpUrl.replace(/\/+$/, "");
|
|
22
|
+
return `${base}/${scope}`;
|
|
23
|
+
}
|
|
24
|
+
function escapeRegExp(value) {
|
|
25
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26
|
+
}
|
|
6
27
|
function isRecord(value) {
|
|
7
28
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
8
29
|
}
|
|
@@ -40,51 +61,109 @@ function backupFileSync(path, mode) {
|
|
|
40
61
|
}
|
|
41
62
|
export function patchClaudeMcpConfig(input) {
|
|
42
63
|
const currentServers = isRecord(input.current.mcpServers) ? input.current.mcpServers : {};
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
64
|
+
const existingOrgx = isRecord(currentServers.orgx) ? currentServers.orgx : {};
|
|
65
|
+
const existingOrgxUrl = typeof existingOrgx.url === "string" ? existingOrgx.url : "";
|
|
66
|
+
const existingOrgxType = typeof existingOrgx.type === "string" ? existingOrgx.type : "";
|
|
67
|
+
const existing = isRecord(currentServers[ORGX_LOCAL_MCP_KEY]) ? currentServers[ORGX_LOCAL_MCP_KEY] : {};
|
|
68
|
+
const priorUrl = typeof existing.url === "string" ? existing.url : "";
|
|
69
|
+
const priorType = typeof existing.type === "string" ? existing.type : "";
|
|
70
|
+
// Ensure hosted OrgX is available alongside the local proxy. Avoid overwriting
|
|
71
|
+
// custom `orgx` entries unless it's clearly redundant (pointing at the same
|
|
72
|
+
// local proxy URL we install under `orgx-openclaw`).
|
|
73
|
+
const shouldSetHostedOrgx = !isRecord(currentServers.orgx) ||
|
|
74
|
+
(existingOrgxUrl === input.localMcpUrl && existingOrgxType === "http");
|
|
75
|
+
const nextOrgxEntry = {
|
|
76
|
+
...existingOrgx,
|
|
77
|
+
type: "http",
|
|
78
|
+
url: ORGX_HOSTED_MCP_URL,
|
|
79
|
+
description: typeof existingOrgx.description === "string" && existingOrgx.description.trim().length > 0
|
|
80
|
+
? existingOrgx.description
|
|
81
|
+
: "OrgX cloud MCP (OAuth)",
|
|
82
|
+
};
|
|
83
|
+
const nextEntry = {
|
|
84
|
+
...existing,
|
|
48
85
|
type: "http",
|
|
49
86
|
url: input.localMcpUrl,
|
|
50
|
-
description: typeof
|
|
51
|
-
?
|
|
87
|
+
description: typeof existing.description === "string" && existing.description.trim().length > 0
|
|
88
|
+
? existing.description
|
|
52
89
|
: "OrgX platform via local OpenClaw plugin (no OAuth)",
|
|
53
90
|
};
|
|
91
|
+
const updatedScopes = [];
|
|
92
|
+
const scopedEntries = {};
|
|
93
|
+
for (const scope of ORGX_LOCAL_MCP_SCOPES) {
|
|
94
|
+
const key = scopedMcpServerKey(scope);
|
|
95
|
+
const expectedUrl = scopedMcpUrl(input.localMcpUrl, scope);
|
|
96
|
+
const existingScoped = isRecord(currentServers[key]) ? currentServers[key] : {};
|
|
97
|
+
const priorUrl = typeof existingScoped.url === "string" ? existingScoped.url : "";
|
|
98
|
+
const priorType = typeof existingScoped.type === "string" ? existingScoped.type : "";
|
|
99
|
+
const nextScoped = {
|
|
100
|
+
...existingScoped,
|
|
101
|
+
type: "http",
|
|
102
|
+
url: expectedUrl,
|
|
103
|
+
description: typeof existingScoped.description === "string" && existingScoped.description.trim().length > 0
|
|
104
|
+
? existingScoped.description
|
|
105
|
+
: `OrgX platform via local OpenClaw plugin (${scope} scope)`,
|
|
106
|
+
};
|
|
107
|
+
scopedEntries[key] = nextScoped;
|
|
108
|
+
if (priorUrl !== expectedUrl || priorType !== "http") {
|
|
109
|
+
updatedScopes.push(scope);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
54
112
|
const nextServers = {
|
|
55
113
|
...currentServers,
|
|
56
|
-
orgx:
|
|
114
|
+
...(shouldSetHostedOrgx ? { orgx: nextOrgxEntry } : {}),
|
|
115
|
+
[ORGX_LOCAL_MCP_KEY]: nextEntry,
|
|
116
|
+
...scopedEntries,
|
|
57
117
|
};
|
|
58
118
|
const next = {
|
|
59
119
|
...input.current,
|
|
60
120
|
mcpServers: nextServers,
|
|
61
121
|
};
|
|
62
|
-
const
|
|
122
|
+
const updatedLocal = priorUrl !== input.localMcpUrl || priorType !== "http";
|
|
123
|
+
const updatedHosted = shouldSetHostedOrgx &&
|
|
124
|
+
(existingOrgxUrl !== ORGX_HOSTED_MCP_URL || existingOrgxType !== "http");
|
|
125
|
+
const updated = updatedLocal || updatedHosted || updatedScopes.length > 0;
|
|
63
126
|
return { updated, next };
|
|
64
127
|
}
|
|
65
128
|
export function patchCursorMcpConfig(input) {
|
|
66
129
|
const currentServers = isRecord(input.current.mcpServers) ? input.current.mcpServers : {};
|
|
67
|
-
const
|
|
68
|
-
const existing = isRecord(currentServers[key]) ? currentServers[key] : {};
|
|
130
|
+
const existing = isRecord(currentServers[ORGX_LOCAL_MCP_KEY]) ? currentServers[ORGX_LOCAL_MCP_KEY] : {};
|
|
69
131
|
const priorUrl = typeof existing.url === "string" ? existing.url : "";
|
|
70
132
|
const nextEntry = {
|
|
71
133
|
...existing,
|
|
72
134
|
url: input.localMcpUrl,
|
|
73
135
|
};
|
|
136
|
+
const scopedEntries = {};
|
|
137
|
+
let updatedScopes = false;
|
|
138
|
+
for (const scope of ORGX_LOCAL_MCP_SCOPES) {
|
|
139
|
+
const key = scopedMcpServerKey(scope);
|
|
140
|
+
const expectedUrl = scopedMcpUrl(input.localMcpUrl, scope);
|
|
141
|
+
const existingScoped = isRecord(currentServers[key]) ? currentServers[key] : {};
|
|
142
|
+
const priorScopedUrl = typeof existingScoped.url === "string" ? existingScoped.url : "";
|
|
143
|
+
scopedEntries[key] = {
|
|
144
|
+
...existingScoped,
|
|
145
|
+
url: expectedUrl,
|
|
146
|
+
};
|
|
147
|
+
if (priorScopedUrl !== expectedUrl)
|
|
148
|
+
updatedScopes = true;
|
|
149
|
+
}
|
|
74
150
|
const nextServers = {
|
|
75
151
|
...currentServers,
|
|
76
|
-
[
|
|
152
|
+
[ORGX_LOCAL_MCP_KEY]: nextEntry,
|
|
153
|
+
...scopedEntries,
|
|
77
154
|
};
|
|
78
155
|
const next = {
|
|
79
156
|
...input.current,
|
|
80
157
|
mcpServers: nextServers,
|
|
81
158
|
};
|
|
82
|
-
const updated = priorUrl !== input.localMcpUrl;
|
|
159
|
+
const updated = priorUrl !== input.localMcpUrl || updatedScopes;
|
|
83
160
|
return { updated, next };
|
|
84
161
|
}
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const
|
|
162
|
+
function upsertCodexMcpServerSection(input) {
|
|
163
|
+
const currentText = input.current;
|
|
164
|
+
const lines = currentText.split(/\r?\n/);
|
|
165
|
+
const escapedKey = escapeRegExp(input.key);
|
|
166
|
+
const headerRegex = new RegExp(`^\\[mcp_servers\\.(?:"${escapedKey}"|${escapedKey})\\]\\s*$`);
|
|
88
167
|
let headerIndex = -1;
|
|
89
168
|
for (let i = 0; i < lines.length; i += 1) {
|
|
90
169
|
if (headerRegex.test(lines[i].trim())) {
|
|
@@ -92,15 +171,10 @@ export function patchCodexConfigToml(input) {
|
|
|
92
171
|
break;
|
|
93
172
|
}
|
|
94
173
|
}
|
|
95
|
-
const urlLine = `url = "${input.
|
|
174
|
+
const urlLine = `url = "${input.url}"`;
|
|
96
175
|
if (headerIndex === -1) {
|
|
97
|
-
const suffix = [
|
|
98
|
-
|
|
99
|
-
"[mcp_servers.orgx]",
|
|
100
|
-
urlLine,
|
|
101
|
-
"",
|
|
102
|
-
].join("\n");
|
|
103
|
-
const normalized = input.current.endsWith("\n") ? input.current : `${input.current}\n`;
|
|
176
|
+
const suffix = ["", `[mcp_servers.\"${input.key}\"]`, urlLine, ""].join("\n");
|
|
177
|
+
const normalized = currentText.endsWith("\n") ? currentText : `${currentText}\n`;
|
|
104
178
|
return { updated: true, next: `${normalized}${suffix}` };
|
|
105
179
|
}
|
|
106
180
|
let sectionEnd = lines.length;
|
|
@@ -127,9 +201,67 @@ export function patchCodexConfigToml(input) {
|
|
|
127
201
|
else {
|
|
128
202
|
lines.splice(headerIndex + 1, 0, urlLine);
|
|
129
203
|
updated = true;
|
|
204
|
+
// Recalculate sectionEnd after splice
|
|
205
|
+
sectionEnd = lines.length;
|
|
206
|
+
for (let i = headerIndex + 1; i < lines.length; i += 1) {
|
|
207
|
+
if (lines[i].trim().startsWith("[")) {
|
|
208
|
+
sectionEnd = i;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Strip stale stdio-transport fields that conflict with url-only entries.
|
|
214
|
+
// Codex rejects `url` when `command`/`args` are present (stdio transport).
|
|
215
|
+
const staleFieldRegex = /^\s*(command|args|startup_timeout_sec)\s*=/;
|
|
216
|
+
for (let i = sectionEnd - 1; i > headerIndex; i -= 1) {
|
|
217
|
+
if (staleFieldRegex.test(lines[i])) {
|
|
218
|
+
lines.splice(i, 1);
|
|
219
|
+
updated = true;
|
|
220
|
+
}
|
|
130
221
|
}
|
|
131
222
|
return { updated, next: `${lines.join("\n")}\n` };
|
|
132
223
|
}
|
|
224
|
+
export function patchCodexConfigToml(input) {
|
|
225
|
+
let current = input.current;
|
|
226
|
+
let updatedHosted = false;
|
|
227
|
+
// If the hosted OrgX entry is missing entirely, add a sensible default. This is
|
|
228
|
+
// a no-op if the user already has `orgx` pointed at staging or another URL.
|
|
229
|
+
const hostedHeaderRegex = /^\[mcp_servers\.(?:"orgx"|orgx)\]\s*$/;
|
|
230
|
+
{
|
|
231
|
+
const lines = current.split(/\r?\n/);
|
|
232
|
+
const hasHosted = lines.some((line) => hostedHeaderRegex.test(line.trim()));
|
|
233
|
+
if (!hasHosted) {
|
|
234
|
+
const hostedUrlLine = `url = "${ORGX_HOSTED_MCP_URL}"`;
|
|
235
|
+
const suffix = [
|
|
236
|
+
...(current.trim().length === 0 ? [] : [""]),
|
|
237
|
+
"[mcp_servers.orgx]",
|
|
238
|
+
hostedUrlLine,
|
|
239
|
+
"",
|
|
240
|
+
].join("\n");
|
|
241
|
+
const normalized = current.endsWith("\n") ? current : `${current}\n`;
|
|
242
|
+
current = `${normalized}${suffix}`;
|
|
243
|
+
updatedHosted = true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
let updated = false;
|
|
247
|
+
const base = upsertCodexMcpServerSection({
|
|
248
|
+
current,
|
|
249
|
+
key: ORGX_LOCAL_MCP_KEY,
|
|
250
|
+
url: input.localMcpUrl,
|
|
251
|
+
});
|
|
252
|
+
updated = updated || base.updated;
|
|
253
|
+
current = base.next;
|
|
254
|
+
for (const scope of ORGX_LOCAL_MCP_SCOPES) {
|
|
255
|
+
const next = upsertCodexMcpServerSection({
|
|
256
|
+
current,
|
|
257
|
+
key: scopedMcpServerKey(scope),
|
|
258
|
+
url: scopedMcpUrl(input.localMcpUrl, scope),
|
|
259
|
+
});
|
|
260
|
+
updated = updated || next.updated;
|
|
261
|
+
current = next.next;
|
|
262
|
+
}
|
|
263
|
+
return { updated: updatedHosted || updated, next: current };
|
|
264
|
+
}
|
|
133
265
|
export async function autoConfigureDetectedMcpClients(input) {
|
|
134
266
|
const logger = input.logger ?? {};
|
|
135
267
|
const home = input.homeDir ?? homedir();
|
|
@@ -30,9 +30,26 @@ export type RegisteredTool = {
|
|
|
30
30
|
parameters: Record<string, unknown>;
|
|
31
31
|
execute: (callId: string, params?: unknown) => Promise<ToolResult>;
|
|
32
32
|
};
|
|
33
|
+
type PromptRole = "system" | "user" | "assistant";
|
|
34
|
+
type PromptMessage = {
|
|
35
|
+
role: PromptRole;
|
|
36
|
+
content: string;
|
|
37
|
+
};
|
|
38
|
+
export type RegisteredPrompt = {
|
|
39
|
+
name: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
arguments?: Array<{
|
|
42
|
+
name: string;
|
|
43
|
+
description?: string;
|
|
44
|
+
required?: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
messages: PromptMessage[];
|
|
47
|
+
};
|
|
33
48
|
export declare function createMcpHttpHandler(input: {
|
|
34
49
|
tools: Map<string, RegisteredTool>;
|
|
50
|
+
prompts?: Map<string, RegisteredPrompt>;
|
|
35
51
|
logger?: Logger;
|
|
36
52
|
serverName: string;
|
|
37
53
|
serverVersion: string;
|
|
38
54
|
}): (req: PluginRequest, res: PluginResponse) => Promise<boolean>;
|
|
55
|
+
export {};
|
package/dist/mcp-http-handler.js
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
const DEFAULT_PROTOCOL_VERSION = "2024-11-05";
|
|
3
|
+
// Domain-scoped MCP servers are meant to be "default safe". The unscoped
|
|
4
|
+
// `/orgx/mcp` endpoint remains available for power users / debugging.
|
|
5
|
+
//
|
|
6
|
+
// NOTE: This scopes only the tools exposed by this plugin (OrgX reporting + mutation).
|
|
7
|
+
// It cannot restrict OpenClaw-native tools (filesystem, shell, etc).
|
|
8
|
+
const ORGX_MCP_ALLOWED_TOOLS_BY_SCOPE = {
|
|
9
|
+
engineering: [
|
|
10
|
+
"orgx_status",
|
|
11
|
+
"orgx_sync",
|
|
12
|
+
"orgx_emit_activity",
|
|
13
|
+
"orgx_report_progress",
|
|
14
|
+
"orgx_register_artifact",
|
|
15
|
+
"orgx_request_decision",
|
|
16
|
+
"orgx_spawn_check",
|
|
17
|
+
],
|
|
18
|
+
product: [
|
|
19
|
+
"orgx_status",
|
|
20
|
+
"orgx_sync",
|
|
21
|
+
"orgx_emit_activity",
|
|
22
|
+
"orgx_report_progress",
|
|
23
|
+
"orgx_register_artifact",
|
|
24
|
+
"orgx_request_decision",
|
|
25
|
+
"orgx_spawn_check",
|
|
26
|
+
],
|
|
27
|
+
design: [
|
|
28
|
+
"orgx_status",
|
|
29
|
+
"orgx_sync",
|
|
30
|
+
"orgx_emit_activity",
|
|
31
|
+
"orgx_report_progress",
|
|
32
|
+
"orgx_register_artifact",
|
|
33
|
+
"orgx_request_decision",
|
|
34
|
+
"orgx_spawn_check",
|
|
35
|
+
],
|
|
36
|
+
marketing: [
|
|
37
|
+
"orgx_status",
|
|
38
|
+
"orgx_sync",
|
|
39
|
+
"orgx_emit_activity",
|
|
40
|
+
"orgx_report_progress",
|
|
41
|
+
"orgx_register_artifact",
|
|
42
|
+
"orgx_request_decision",
|
|
43
|
+
"orgx_spawn_check",
|
|
44
|
+
],
|
|
45
|
+
sales: [
|
|
46
|
+
"orgx_status",
|
|
47
|
+
"orgx_sync",
|
|
48
|
+
"orgx_emit_activity",
|
|
49
|
+
"orgx_report_progress",
|
|
50
|
+
"orgx_register_artifact",
|
|
51
|
+
"orgx_request_decision",
|
|
52
|
+
"orgx_spawn_check",
|
|
53
|
+
],
|
|
54
|
+
operations: [
|
|
55
|
+
"orgx_status",
|
|
56
|
+
"orgx_sync",
|
|
57
|
+
"orgx_emit_activity",
|
|
58
|
+
"orgx_report_progress",
|
|
59
|
+
"orgx_register_artifact",
|
|
60
|
+
"orgx_request_decision",
|
|
61
|
+
"orgx_spawn_check",
|
|
62
|
+
// Operations is allowed to do explicit changesets for remediation/runbooks.
|
|
63
|
+
"orgx_apply_changeset",
|
|
64
|
+
],
|
|
65
|
+
orchestration: [
|
|
66
|
+
"orgx_status",
|
|
67
|
+
"orgx_sync",
|
|
68
|
+
"orgx_emit_activity",
|
|
69
|
+
"orgx_report_progress",
|
|
70
|
+
"orgx_register_artifact",
|
|
71
|
+
"orgx_request_decision",
|
|
72
|
+
"orgx_spawn_check",
|
|
73
|
+
// Orchestrator is the primary mutation surface by design.
|
|
74
|
+
"orgx_apply_changeset",
|
|
75
|
+
],
|
|
76
|
+
};
|
|
3
77
|
function isRecord(value) {
|
|
4
78
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
5
79
|
}
|
|
@@ -38,6 +112,28 @@ function normalizePath(rawUrl) {
|
|
|
38
112
|
const [path] = rawUrl.split("?", 2);
|
|
39
113
|
return path || "/";
|
|
40
114
|
}
|
|
115
|
+
function parseScopeKey(url) {
|
|
116
|
+
// Supported paths:
|
|
117
|
+
// - /orgx/mcp (unscoped)
|
|
118
|
+
// - /orgx/mcp/<scope> (domain-scoped)
|
|
119
|
+
if (!url.startsWith("/orgx/mcp/"))
|
|
120
|
+
return null;
|
|
121
|
+
const rest = url.slice("/orgx/mcp/".length);
|
|
122
|
+
const key = rest.split("/", 1)[0]?.trim() ?? "";
|
|
123
|
+
if (!key)
|
|
124
|
+
return null;
|
|
125
|
+
if (!Object.prototype.hasOwnProperty.call(ORGX_MCP_ALLOWED_TOOLS_BY_SCOPE, key))
|
|
126
|
+
return null;
|
|
127
|
+
return key;
|
|
128
|
+
}
|
|
129
|
+
function resolveToolScope(scopeKey) {
|
|
130
|
+
if (!scopeKey)
|
|
131
|
+
return null;
|
|
132
|
+
return {
|
|
133
|
+
key: scopeKey,
|
|
134
|
+
allowedTools: new Set(ORGX_MCP_ALLOWED_TOOLS_BY_SCOPE[scopeKey]),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
41
137
|
async function readRequestBodyBuffer(req) {
|
|
42
138
|
const body = req.body;
|
|
43
139
|
if (typeof body === "string")
|
|
@@ -131,13 +227,15 @@ async function handleRpcMessage(input) {
|
|
|
131
227
|
if (method === "initialize") {
|
|
132
228
|
const requestedProtocol = typeof params.protocolVersion === "string" ? params.protocolVersion : null;
|
|
133
229
|
const protocolVersion = requestedProtocol?.trim() || DEFAULT_PROTOCOL_VERSION;
|
|
230
|
+
const scopedServerName = input.toolScope ? `${input.serverName}/${input.toolScope.key}` : input.serverName;
|
|
134
231
|
return jsonRpcResult(id, {
|
|
135
232
|
protocolVersion,
|
|
136
233
|
capabilities: {
|
|
137
234
|
tools: {},
|
|
235
|
+
prompts: {},
|
|
138
236
|
},
|
|
139
237
|
serverInfo: {
|
|
140
|
-
name:
|
|
238
|
+
name: scopedServerName,
|
|
141
239
|
version: input.serverVersion,
|
|
142
240
|
},
|
|
143
241
|
});
|
|
@@ -146,6 +244,15 @@ async function handleRpcMessage(input) {
|
|
|
146
244
|
return jsonRpcResult(id, { ok: true });
|
|
147
245
|
}
|
|
148
246
|
if (method === "tools/list") {
|
|
247
|
+
if (input.toolScope) {
|
|
248
|
+
const scopedTools = new Map();
|
|
249
|
+
for (const name of input.toolScope.allowedTools) {
|
|
250
|
+
const tool = input.tools.get(name);
|
|
251
|
+
if (tool)
|
|
252
|
+
scopedTools.set(name, tool);
|
|
253
|
+
}
|
|
254
|
+
return jsonRpcResult(id, { tools: buildToolsList(scopedTools) });
|
|
255
|
+
}
|
|
149
256
|
return jsonRpcResult(id, {
|
|
150
257
|
tools: buildToolsList(input.tools),
|
|
151
258
|
});
|
|
@@ -155,6 +262,9 @@ async function handleRpcMessage(input) {
|
|
|
155
262
|
if (!toolName) {
|
|
156
263
|
return jsonRpcError(id, -32602, "Missing tool name");
|
|
157
264
|
}
|
|
265
|
+
if (input.toolScope && !input.toolScope.allowedTools.has(toolName)) {
|
|
266
|
+
return jsonRpcError(id, -32601, `Tool not available in scope '${input.toolScope.key}': ${toolName}`);
|
|
267
|
+
}
|
|
158
268
|
const tool = input.tools.get(toolName) ?? null;
|
|
159
269
|
if (!tool) {
|
|
160
270
|
return jsonRpcError(id, -32601, `Tool not found: ${toolName}`);
|
|
@@ -188,7 +298,28 @@ async function handleRpcMessage(input) {
|
|
|
188
298
|
return jsonRpcResult(id, { resources: [] });
|
|
189
299
|
}
|
|
190
300
|
if (method === "prompts/list") {
|
|
191
|
-
|
|
301
|
+
const prompts = Array.from(input.prompts.values())
|
|
302
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
303
|
+
.map((prompt) => ({
|
|
304
|
+
name: prompt.name,
|
|
305
|
+
description: prompt.description ?? "",
|
|
306
|
+
arguments: Array.isArray(prompt.arguments) ? prompt.arguments : [],
|
|
307
|
+
}));
|
|
308
|
+
return jsonRpcResult(id, { prompts });
|
|
309
|
+
}
|
|
310
|
+
if (method === "prompts/get") {
|
|
311
|
+
const promptName = typeof params.name === "string" ? params.name.trim() : "";
|
|
312
|
+
if (!promptName) {
|
|
313
|
+
return jsonRpcError(id, -32602, "Missing prompt name");
|
|
314
|
+
}
|
|
315
|
+
const prompt = input.prompts.get(promptName) ?? null;
|
|
316
|
+
if (!prompt) {
|
|
317
|
+
return jsonRpcError(id, -32601, `Prompt not found: ${promptName}`);
|
|
318
|
+
}
|
|
319
|
+
return jsonRpcResult(id, {
|
|
320
|
+
description: prompt.description ?? "",
|
|
321
|
+
messages: prompt.messages,
|
|
322
|
+
});
|
|
192
323
|
}
|
|
193
324
|
if (method.startsWith("notifications/")) {
|
|
194
325
|
return null;
|
|
@@ -197,6 +328,7 @@ async function handleRpcMessage(input) {
|
|
|
197
328
|
}
|
|
198
329
|
export function createMcpHttpHandler(input) {
|
|
199
330
|
const logger = input.logger ?? {};
|
|
331
|
+
const prompts = input.prompts ?? new Map();
|
|
200
332
|
return async function handler(req, res) {
|
|
201
333
|
const method = (req.method ?? "GET").toUpperCase();
|
|
202
334
|
const rawUrl = req.url ?? "/";
|
|
@@ -204,6 +336,12 @@ export function createMcpHttpHandler(input) {
|
|
|
204
336
|
if (!(url === "/orgx/mcp" || url.startsWith("/orgx/mcp/"))) {
|
|
205
337
|
return false;
|
|
206
338
|
}
|
|
339
|
+
const scopeKey = parseScopeKey(url);
|
|
340
|
+
const toolScope = resolveToolScope(scopeKey);
|
|
341
|
+
if (url.startsWith("/orgx/mcp/") && !scopeKey) {
|
|
342
|
+
sendText(res, 404, `Unknown OrgX MCP scope. Supported: ${Object.keys(ORGX_MCP_ALLOWED_TOOLS_BY_SCOPE).join(", ")}\n`);
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
207
345
|
if (method === "OPTIONS") {
|
|
208
346
|
res.writeHead(204, {
|
|
209
347
|
"cache-control": "no-store",
|
|
@@ -212,7 +350,8 @@ export function createMcpHttpHandler(input) {
|
|
|
212
350
|
return true;
|
|
213
351
|
}
|
|
214
352
|
if (method === "GET") {
|
|
215
|
-
|
|
353
|
+
const suffix = toolScope ? ` (scope: ${toolScope.key})` : "";
|
|
354
|
+
sendText(res, 200, `OrgX Local MCP bridge is running${suffix}.\n`);
|
|
216
355
|
return true;
|
|
217
356
|
}
|
|
218
357
|
if (method !== "POST") {
|
|
@@ -234,9 +373,11 @@ export function createMcpHttpHandler(input) {
|
|
|
234
373
|
const response = await handleRpcMessage({
|
|
235
374
|
message,
|
|
236
375
|
tools: input.tools,
|
|
376
|
+
prompts,
|
|
237
377
|
logger,
|
|
238
378
|
serverName: input.serverName,
|
|
239
379
|
serverVersion: input.serverVersion,
|
|
380
|
+
toolScope,
|
|
240
381
|
});
|
|
241
382
|
if (response)
|
|
242
383
|
responses.push(response);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type NextUpPinnedEntry = {
|
|
2
|
+
initiativeId: string;
|
|
3
|
+
workstreamId: string;
|
|
4
|
+
preferredTaskId: string | null;
|
|
5
|
+
preferredMilestoneId: string | null;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
updatedAt: string;
|
|
8
|
+
};
|
|
9
|
+
type PersistedNextUpQueue = {
|
|
10
|
+
version: 1;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
pins: NextUpPinnedEntry[];
|
|
13
|
+
};
|
|
14
|
+
export declare function readNextUpQueuePins(): PersistedNextUpQueue;
|
|
15
|
+
export declare function upsertNextUpQueuePin(input: {
|
|
16
|
+
initiativeId: string;
|
|
17
|
+
workstreamId: string;
|
|
18
|
+
preferredTaskId?: string | null;
|
|
19
|
+
preferredMilestoneId?: string | null;
|
|
20
|
+
}): PersistedNextUpQueue;
|
|
21
|
+
export declare function removeNextUpQueuePin(input: {
|
|
22
|
+
initiativeId: string;
|
|
23
|
+
workstreamId: string;
|
|
24
|
+
}): PersistedNextUpQueue;
|
|
25
|
+
export declare function setNextUpQueuePinOrder(input: {
|
|
26
|
+
order: Array<{
|
|
27
|
+
initiativeId: string;
|
|
28
|
+
workstreamId: string;
|
|
29
|
+
}>;
|
|
30
|
+
}): PersistedNextUpQueue;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, } from "node:fs";
|
|
2
|
+
import { getOrgxPluginConfigDir, getOrgxPluginConfigPath } from "./paths.js";
|
|
3
|
+
import { backupCorruptFileSync, writeJsonFileAtomicSync } from "./fs-utils.js";
|
|
4
|
+
const MAX_PINS = 240;
|
|
5
|
+
function storeDir() {
|
|
6
|
+
return getOrgxPluginConfigDir();
|
|
7
|
+
}
|
|
8
|
+
function storeFile() {
|
|
9
|
+
return getOrgxPluginConfigPath("next-up-queue.json");
|
|
10
|
+
}
|
|
11
|
+
function ensureStoreDir() {
|
|
12
|
+
const dir = storeDir();
|
|
13
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
14
|
+
try {
|
|
15
|
+
chmodSync(dir, 0o700);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// best effort
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function parseJson(value) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(value);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function normalizeNullableString(value) {
|
|
30
|
+
if (typeof value !== "string")
|
|
31
|
+
return null;
|
|
32
|
+
const trimmed = value.trim();
|
|
33
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
34
|
+
}
|
|
35
|
+
function normalizeEntry(input) {
|
|
36
|
+
return {
|
|
37
|
+
initiativeId: input.initiativeId.trim(),
|
|
38
|
+
workstreamId: input.workstreamId.trim(),
|
|
39
|
+
preferredTaskId: normalizeNullableString(input.preferredTaskId),
|
|
40
|
+
preferredMilestoneId: normalizeNullableString(input.preferredMilestoneId),
|
|
41
|
+
createdAt: input.createdAt,
|
|
42
|
+
updatedAt: input.updatedAt,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function readNextUpQueuePins() {
|
|
46
|
+
const file = storeFile();
|
|
47
|
+
try {
|
|
48
|
+
if (!existsSync(file)) {
|
|
49
|
+
return { version: 1, updatedAt: new Date().toISOString(), pins: [] };
|
|
50
|
+
}
|
|
51
|
+
const raw = readFileSync(file, "utf8");
|
|
52
|
+
const parsed = parseJson(raw);
|
|
53
|
+
if (!parsed || typeof parsed !== "object") {
|
|
54
|
+
backupCorruptFileSync(file);
|
|
55
|
+
return { version: 1, updatedAt: new Date().toISOString(), pins: [] };
|
|
56
|
+
}
|
|
57
|
+
const pins = Array.isArray(parsed.pins) ? parsed.pins : [];
|
|
58
|
+
return {
|
|
59
|
+
version: 1,
|
|
60
|
+
updatedAt: typeof parsed.updatedAt === "string"
|
|
61
|
+
? parsed.updatedAt
|
|
62
|
+
: new Date().toISOString(),
|
|
63
|
+
pins: pins
|
|
64
|
+
.filter((entry) => Boolean(entry && typeof entry === "object"))
|
|
65
|
+
.map((entry) => normalizeEntry(entry)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return { version: 1, updatedAt: new Date().toISOString(), pins: [] };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function upsertNextUpQueuePin(input) {
|
|
73
|
+
const initiativeId = input.initiativeId.trim();
|
|
74
|
+
const workstreamId = input.workstreamId.trim();
|
|
75
|
+
if (!initiativeId || !workstreamId) {
|
|
76
|
+
return readNextUpQueuePins();
|
|
77
|
+
}
|
|
78
|
+
ensureStoreDir();
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
const next = readNextUpQueuePins();
|
|
81
|
+
const key = `${initiativeId}:${workstreamId}`;
|
|
82
|
+
const existing = next.pins.find((pin) => `${pin.initiativeId}:${pin.workstreamId}` === key);
|
|
83
|
+
const updated = normalizeEntry({
|
|
84
|
+
initiativeId,
|
|
85
|
+
workstreamId,
|
|
86
|
+
preferredTaskId: input.preferredTaskId ?? existing?.preferredTaskId ?? null,
|
|
87
|
+
preferredMilestoneId: input.preferredMilestoneId ?? existing?.preferredMilestoneId ?? null,
|
|
88
|
+
createdAt: existing?.createdAt ?? now,
|
|
89
|
+
updatedAt: now,
|
|
90
|
+
});
|
|
91
|
+
next.pins = [updated, ...next.pins.filter((pin) => `${pin.initiativeId}:${pin.workstreamId}` !== key)].slice(0, MAX_PINS);
|
|
92
|
+
next.updatedAt = now;
|
|
93
|
+
try {
|
|
94
|
+
writeJsonFileAtomicSync(storeFile(), next, 0o600);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// best effort
|
|
98
|
+
}
|
|
99
|
+
return next;
|
|
100
|
+
}
|
|
101
|
+
export function removeNextUpQueuePin(input) {
|
|
102
|
+
const initiativeId = input.initiativeId.trim();
|
|
103
|
+
const workstreamId = input.workstreamId.trim();
|
|
104
|
+
if (!initiativeId || !workstreamId) {
|
|
105
|
+
return readNextUpQueuePins();
|
|
106
|
+
}
|
|
107
|
+
ensureStoreDir();
|
|
108
|
+
const next = readNextUpQueuePins();
|
|
109
|
+
const key = `${initiativeId}:${workstreamId}`;
|
|
110
|
+
const filtered = next.pins.filter((pin) => `${pin.initiativeId}:${pin.workstreamId}` !== key);
|
|
111
|
+
if (filtered.length === next.pins.length)
|
|
112
|
+
return next;
|
|
113
|
+
next.pins = filtered;
|
|
114
|
+
next.updatedAt = new Date().toISOString();
|
|
115
|
+
try {
|
|
116
|
+
writeJsonFileAtomicSync(storeFile(), next, 0o600);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// best effort
|
|
120
|
+
}
|
|
121
|
+
return next;
|
|
122
|
+
}
|
|
123
|
+
export function setNextUpQueuePinOrder(input) {
|
|
124
|
+
ensureStoreDir();
|
|
125
|
+
const next = readNextUpQueuePins();
|
|
126
|
+
const now = new Date().toISOString();
|
|
127
|
+
const byKey = new Map(next.pins.map((pin) => [`${pin.initiativeId}:${pin.workstreamId}`, pin]));
|
|
128
|
+
const ordered = [];
|
|
129
|
+
const seen = new Set();
|
|
130
|
+
for (const entry of input.order) {
|
|
131
|
+
const initiativeId = (entry.initiativeId ?? "").trim();
|
|
132
|
+
const workstreamId = (entry.workstreamId ?? "").trim();
|
|
133
|
+
if (!initiativeId || !workstreamId)
|
|
134
|
+
continue;
|
|
135
|
+
const key = `${initiativeId}:${workstreamId}`;
|
|
136
|
+
if (seen.has(key))
|
|
137
|
+
continue;
|
|
138
|
+
seen.add(key);
|
|
139
|
+
const pin = byKey.get(key);
|
|
140
|
+
if (pin) {
|
|
141
|
+
ordered.push(pin);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
ordered.push({
|
|
145
|
+
initiativeId,
|
|
146
|
+
workstreamId,
|
|
147
|
+
preferredTaskId: null,
|
|
148
|
+
preferredMilestoneId: null,
|
|
149
|
+
createdAt: now,
|
|
150
|
+
updatedAt: now,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const pin of next.pins) {
|
|
155
|
+
const key = `${pin.initiativeId}:${pin.workstreamId}`;
|
|
156
|
+
if (seen.has(key))
|
|
157
|
+
continue;
|
|
158
|
+
ordered.push(pin);
|
|
159
|
+
}
|
|
160
|
+
next.pins = ordered.slice(0, MAX_PINS);
|
|
161
|
+
next.updatedAt = now;
|
|
162
|
+
try {
|
|
163
|
+
writeJsonFileAtomicSync(storeFile(), next, 0o600);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// best effort
|
|
167
|
+
}
|
|
168
|
+
return next;
|
|
169
|
+
}
|