@engineeros/connector 0.8.2 → 0.8.4
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 +2 -0
- package/bin/engineeros-connector.mjs +18 -1
- package/package.json +2 -2
- package/src/config.mjs +7 -0
- package/src/mcp-server.mjs +256 -0
- package/src/runner.mjs +28 -0
package/README.md
CHANGED
|
@@ -49,6 +49,8 @@ Credentials are stored per workspace under `~/.engineeros/connectors` with owner
|
|
|
49
49
|
|
|
50
50
|
Keep the connector online to receive Goals assigned from EngineerOS. Each Goal runs in an isolated worktree below `~/.engineeros/runs`. Cancellation stops Codex. The connector returns changed paths, a bounded diff, and the exact repository ZIP; a human still performs independent attestation.
|
|
51
51
|
|
|
52
|
+
During the writable implementation phase, Codex receives an authenticated EngineerOS MCP server automatically. It can list, read, create, update, reclassify, soft-delete, and materialize project artifacts into canonical Product records through the same repository boundary used by Copilot. The backend accepts those calls only while the assigned Goal is running. Read-only project prompts and the independent verification phase do not receive mutation tools.
|
|
53
|
+
|
|
52
54
|
Requirements: Node.js 22 or newer, Git, and an authenticated Codex CLI (`codex login`).
|
|
53
55
|
|
|
54
56
|
## Execution profiles
|
|
@@ -25,9 +25,26 @@ import {
|
|
|
25
25
|
} from "../src/connection.mjs";
|
|
26
26
|
import { advertisedCapabilities } from "../src/capabilities.mjs";
|
|
27
27
|
import { parseConnectorArgs } from "../src/cli-args.mjs";
|
|
28
|
+
import { runMcpServer } from "../src/mcp-server.mjs";
|
|
29
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
28
30
|
|
|
29
31
|
const { command, positional, flags } = parseConnectorArgs(process.argv.slice(2));
|
|
30
32
|
|
|
33
|
+
if (command === "mcp") {
|
|
34
|
+
const config = await loadConfig(flags.workspace || process.cwd());
|
|
35
|
+
if (!config) fail("This workspace is not paired with EngineerOS.");
|
|
36
|
+
const runId = flags["run-id"] || positional[0];
|
|
37
|
+
if (!runId) fail("Usage: engineeros-connector mcp --run-id RUN_ID [--workspace PATH]");
|
|
38
|
+
await runMcpServer({
|
|
39
|
+
config,
|
|
40
|
+
runId,
|
|
41
|
+
version: packageJson.version,
|
|
42
|
+
input: process.stdin,
|
|
43
|
+
output: process.stdout,
|
|
44
|
+
});
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
if (command === "status") {
|
|
32
49
|
const config = await loadConfig(flags.workspace || process.cwd());
|
|
33
50
|
console.log(
|
|
@@ -80,7 +97,7 @@ if (command === "pair") {
|
|
|
80
97
|
token: config.token,
|
|
81
98
|
};
|
|
82
99
|
} else {
|
|
83
|
-
fail("Use `engineeros-connector pair`, `start`, or `
|
|
100
|
+
fail("Use `engineeros-connector pair`, `start`, `status`, or `mcp`.");
|
|
84
101
|
}
|
|
85
102
|
|
|
86
103
|
let codingAgent;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@engineeros/connector",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"start": "node ./bin/engineeros-connector.mjs",
|
|
18
18
|
"test": "node --test --test-concurrency=1",
|
|
19
|
-
"type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./src/capabilities.mjs && node --check ./src/cli-args.mjs && node --check ./src/connection.mjs && node --check ./src/runner.mjs"
|
|
19
|
+
"type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./src/capabilities.mjs && node --check ./src/cli-args.mjs && node --check ./src/connection.mjs && node --check ./src/mcp-server.mjs && node --check ./src/runner.mjs"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=22"
|
package/src/config.mjs
CHANGED
|
@@ -45,6 +45,13 @@ export function assessmentResultUrl(websocketUrl, connectorId, assessmentId) {
|
|
|
45
45
|
return url.toString();
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
export function artifactToolUrl(websocketUrl, connectorId) {
|
|
49
|
+
const url = new URL(websocketUrl);
|
|
50
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
51
|
+
url.pathname = `/api/v1/agent-connectors/${connectorId}/artifact-tools`;
|
|
52
|
+
return url.toString();
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
export async function loadConfig(workspace = process.cwd()) {
|
|
49
56
|
try {
|
|
50
57
|
return JSON.parse(await readFile(configPath(workspace), "utf8"));
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
|
|
3
|
+
import { artifactToolUrl } from "./config.mjs";
|
|
4
|
+
|
|
5
|
+
const PROTOCOL_VERSION = "2025-06-18";
|
|
6
|
+
|
|
7
|
+
const ARTIFACT_TYPES = [
|
|
8
|
+
"vision",
|
|
9
|
+
"problem",
|
|
10
|
+
"capability",
|
|
11
|
+
"epic",
|
|
12
|
+
"feature",
|
|
13
|
+
"story",
|
|
14
|
+
"architecture",
|
|
15
|
+
"service",
|
|
16
|
+
"api",
|
|
17
|
+
"data_model",
|
|
18
|
+
"event",
|
|
19
|
+
"task",
|
|
20
|
+
"release",
|
|
21
|
+
"risk",
|
|
22
|
+
"pattern",
|
|
23
|
+
"technology",
|
|
24
|
+
"agent",
|
|
25
|
+
"workflow",
|
|
26
|
+
"review",
|
|
27
|
+
"comment",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const artifactTypeSchema = { type: "string", enum: ARTIFACT_TYPES };
|
|
31
|
+
const artifactIdSchema = {
|
|
32
|
+
type: "string",
|
|
33
|
+
format: "uuid",
|
|
34
|
+
description: "The saved EngineerOS artifact ID.",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const artifactTools = [
|
|
38
|
+
{
|
|
39
|
+
name: "engineeros_list_artifacts",
|
|
40
|
+
description: "List saved artifacts in the current EngineerOS project.",
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
artifact_type: artifactTypeSchema,
|
|
45
|
+
status: { type: "string", enum: ["draft", "active", "archived"] },
|
|
46
|
+
page: { type: "integer", minimum: 1 },
|
|
47
|
+
page_size: { type: "integer", minimum: 1, maximum: 50 },
|
|
48
|
+
},
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
},
|
|
51
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: "engineeros_get_artifact",
|
|
55
|
+
description: "Load one saved artifact from the current EngineerOS project.",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: { artifact_id: artifactIdSchema },
|
|
59
|
+
required: ["artifact_id"],
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
},
|
|
62
|
+
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "engineeros_create_artifact",
|
|
66
|
+
description:
|
|
67
|
+
"Create a saved EngineerOS artifact during the currently approved writable Goal.",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: "object",
|
|
70
|
+
properties: {
|
|
71
|
+
artifact_type: artifactTypeSchema,
|
|
72
|
+
name: { type: "string", minLength: 1, maxLength: 500 },
|
|
73
|
+
content: {
|
|
74
|
+
type: "string",
|
|
75
|
+
minLength: 1,
|
|
76
|
+
description: "Complete artifact content as structured Markdown.",
|
|
77
|
+
},
|
|
78
|
+
metadata_info: { type: "object" },
|
|
79
|
+
},
|
|
80
|
+
required: ["artifact_type", "name", "content"],
|
|
81
|
+
additionalProperties: false,
|
|
82
|
+
},
|
|
83
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "engineeros_update_artifact",
|
|
87
|
+
description:
|
|
88
|
+
"Update a saved EngineerOS artifact during the currently approved writable Goal.",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
artifact_id: artifactIdSchema,
|
|
93
|
+
artifact_type: artifactTypeSchema,
|
|
94
|
+
name: { type: "string", minLength: 1, maxLength: 500 },
|
|
95
|
+
content: {
|
|
96
|
+
type: "string",
|
|
97
|
+
minLength: 1,
|
|
98
|
+
description: "Complete replacement content as structured Markdown.",
|
|
99
|
+
},
|
|
100
|
+
metadata_info: { type: "object" },
|
|
101
|
+
},
|
|
102
|
+
required: ["artifact_id"],
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
},
|
|
105
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "engineeros_reclassify_artifact",
|
|
109
|
+
description:
|
|
110
|
+
"Move a saved artifact to another EngineerOS classification during the currently approved writable Goal.",
|
|
111
|
+
inputSchema: {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: {
|
|
114
|
+
artifact_id: artifactIdSchema,
|
|
115
|
+
target_artifact_type: artifactTypeSchema,
|
|
116
|
+
},
|
|
117
|
+
required: ["artifact_id", "target_artifact_type"],
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
},
|
|
120
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "engineeros_materialize_product_artifacts",
|
|
124
|
+
description:
|
|
125
|
+
"Materialize structured Product documents into the canonical EngineerOS Vision, Portfolio capabilities, and project graph during the currently approved writable Goal.",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: "object",
|
|
128
|
+
properties: {},
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
},
|
|
131
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "engineeros_delete_artifact",
|
|
135
|
+
description:
|
|
136
|
+
"Soft-delete a saved EngineerOS artifact during the currently approved writable Goal.",
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: "object",
|
|
139
|
+
properties: { artifact_id: artifactIdSchema },
|
|
140
|
+
required: ["artifact_id"],
|
|
141
|
+
additionalProperties: false,
|
|
142
|
+
},
|
|
143
|
+
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const toolNames = new Map(
|
|
148
|
+
artifactTools.map((tool) => [tool.name, tool.name.replace("engineeros_", "")]),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
export async function handleMcpRequest(message, context) {
|
|
152
|
+
if (message.method === "notifications/initialized") return null;
|
|
153
|
+
if (message.method === "initialize") {
|
|
154
|
+
return success(message.id, {
|
|
155
|
+
protocolVersion: message.params?.protocolVersion || PROTOCOL_VERSION,
|
|
156
|
+
capabilities: { tools: { listChanged: false } },
|
|
157
|
+
serverInfo: { name: "engineeros-connector", version: context.version },
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (message.method === "ping") return success(message.id, {});
|
|
161
|
+
if (message.method === "tools/list") {
|
|
162
|
+
return success(message.id, { tools: artifactTools });
|
|
163
|
+
}
|
|
164
|
+
if (message.method === "tools/call") {
|
|
165
|
+
const tool = toolNames.get(message.params?.name);
|
|
166
|
+
if (!tool) return failure(message.id, -32602, "Unknown EngineerOS artifact tool.");
|
|
167
|
+
try {
|
|
168
|
+
const response = await callArtifactTool(context, tool, message.params?.arguments || {});
|
|
169
|
+
return success(message.id, {
|
|
170
|
+
content: [{ type: "text", text: renderArtifactToolResult(response) }],
|
|
171
|
+
isError: false,
|
|
172
|
+
});
|
|
173
|
+
} catch (error) {
|
|
174
|
+
return success(message.id, {
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: "text",
|
|
178
|
+
text: `# EngineerOS artifact tool failed\n\n${error instanceof Error ? error.message : String(error)}`,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
isError: true,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return failure(message.id, -32601, `Unsupported MCP method: ${message.method}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export async function runMcpServer({ config, runId, version, input, output, fetchImpl = fetch }) {
|
|
189
|
+
const context = { config, runId, version, fetchImpl };
|
|
190
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
191
|
+
for await (const line of lines) {
|
|
192
|
+
if (!line.trim()) continue;
|
|
193
|
+
let response;
|
|
194
|
+
try {
|
|
195
|
+
response = await handleMcpRequest(JSON.parse(line), context);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
response = failure(null, -32700, error instanceof Error ? error.message : String(error));
|
|
198
|
+
}
|
|
199
|
+
if (response) output.write(`${JSON.stringify(response)}\n`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function callArtifactTool(context, tool, arguments_) {
|
|
204
|
+
const response = await context.fetchImpl(
|
|
205
|
+
artifactToolUrl(context.config.server_url, context.config.connector_id),
|
|
206
|
+
{
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: {
|
|
209
|
+
"Content-Type": "application/json",
|
|
210
|
+
Authorization: `Bearer ${context.config.token}`,
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify({ run_id: context.runId, tool, arguments: arguments_ }),
|
|
213
|
+
},
|
|
214
|
+
);
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
const detail = await response.text();
|
|
217
|
+
throw new Error(`EngineerOS rejected ${tool} (${response.status}): ${detail}`);
|
|
218
|
+
}
|
|
219
|
+
return response.json();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function renderArtifactToolResult(result) {
|
|
223
|
+
const lines = ["# EngineerOS artifact tool result", "", result.message || "Completed."];
|
|
224
|
+
if (result.vision_id) lines.push("", `- Vision ID: \`${result.vision_id}\``);
|
|
225
|
+
if (Array.isArray(result.capability_ids) && result.capability_ids.length) {
|
|
226
|
+
lines.push(`- Capability IDs: ${result.capability_ids.map((id) => `\`${id}\``).join(", ")}`);
|
|
227
|
+
}
|
|
228
|
+
if (result.artifact) lines.push("", ...artifactMarkdown(result.artifact));
|
|
229
|
+
if (Array.isArray(result.artifacts)) {
|
|
230
|
+
lines.push("", `## Artifacts (${result.total ?? result.artifacts.length})`, "");
|
|
231
|
+
for (const artifact of result.artifacts) {
|
|
232
|
+
lines.push(`- **${artifact.name}** — \`${artifact.artifact_type}\` — \`${artifact.id}\``);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return lines.join("\n");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function artifactMarkdown(artifact) {
|
|
239
|
+
return [
|
|
240
|
+
`## ${artifact.name}`,
|
|
241
|
+
"",
|
|
242
|
+
`- ID: \`${artifact.id}\``,
|
|
243
|
+
`- Type: \`${artifact.artifact_type}\``,
|
|
244
|
+
`- Status: \`${artifact.status}\``,
|
|
245
|
+
"",
|
|
246
|
+
artifact.content || "",
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function success(id, result) {
|
|
251
|
+
return { jsonrpc: "2.0", id, result };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function failure(id, code, message) {
|
|
255
|
+
return { jsonrpc: "2.0", id, error: { code, message } };
|
|
256
|
+
}
|
package/src/runner.mjs
CHANGED
|
@@ -102,6 +102,8 @@ export async function executeAssignment(assignment, config, callbacks) {
|
|
|
102
102
|
config,
|
|
103
103
|
callbacks,
|
|
104
104
|
execution.profile,
|
|
105
|
+
undefined,
|
|
106
|
+
{ runId: assignment.run_id },
|
|
105
107
|
);
|
|
106
108
|
callbacks.onProcess?.(controller.child);
|
|
107
109
|
await controller.completed;
|
|
@@ -756,6 +758,7 @@ function launchCodexProcess(
|
|
|
756
758
|
profile = {},
|
|
757
759
|
previousSessionId,
|
|
758
760
|
skipGitRepoCheck = false,
|
|
761
|
+
mcpServer,
|
|
759
762
|
) {
|
|
760
763
|
const command =
|
|
761
764
|
process.env.CODEX_BIN ||
|
|
@@ -766,6 +769,7 @@ function launchCodexProcess(
|
|
|
766
769
|
profile,
|
|
767
770
|
previousSessionId,
|
|
768
771
|
skipGitRepoCheck,
|
|
772
|
+
mcpServer,
|
|
769
773
|
});
|
|
770
774
|
const child = spawn(command, args, {
|
|
771
775
|
cwd: workspace,
|
|
@@ -832,6 +836,7 @@ export function codexExecutionArgs({
|
|
|
832
836
|
profile = {},
|
|
833
837
|
previousSessionId,
|
|
834
838
|
skipGitRepoCheck = false,
|
|
839
|
+
mcpServer,
|
|
835
840
|
}) {
|
|
836
841
|
const optionArgs = ["--json"];
|
|
837
842
|
if (skipGitRepoCheck) optionArgs.push("--skip-git-repo-check");
|
|
@@ -842,6 +847,21 @@ export function codexExecutionArgs({
|
|
|
842
847
|
`model_reasoning_effort=${JSON.stringify(profile.reasoning_effort)}`,
|
|
843
848
|
);
|
|
844
849
|
}
|
|
850
|
+
if (mcpServer) {
|
|
851
|
+
optionArgs.push(
|
|
852
|
+
"--config",
|
|
853
|
+
`mcp_servers.engineeros.command=${JSON.stringify(process.execPath)}`,
|
|
854
|
+
"--config",
|
|
855
|
+
`mcp_servers.engineeros.args=${JSON.stringify([
|
|
856
|
+
mcpServer.connectorBin,
|
|
857
|
+
"mcp",
|
|
858
|
+
"--workspace",
|
|
859
|
+
mcpServer.workspace,
|
|
860
|
+
"--run-id",
|
|
861
|
+
mcpServer.runId,
|
|
862
|
+
])}`,
|
|
863
|
+
);
|
|
864
|
+
}
|
|
845
865
|
return previousSessionId
|
|
846
866
|
? ["exec", "resume", ...optionArgs, previousSessionId, "-"]
|
|
847
867
|
: ["exec", ...optionArgs, "--sandbox", sandbox, "-C", workspace, "-"];
|
|
@@ -855,6 +875,7 @@ function launchAgentProcess(
|
|
|
855
875
|
callbacks,
|
|
856
876
|
profile = {},
|
|
857
877
|
previousSessionId,
|
|
878
|
+
mcpContext,
|
|
858
879
|
) {
|
|
859
880
|
if (config.agent_protocol === "acp") {
|
|
860
881
|
if (profile.model || profile.reasoning_effort) {
|
|
@@ -872,6 +893,13 @@ function launchAgentProcess(
|
|
|
872
893
|
profile,
|
|
873
894
|
previousSessionId,
|
|
874
895
|
config.skip_git_repo_check === true,
|
|
896
|
+
mcpContext
|
|
897
|
+
? {
|
|
898
|
+
connectorBin: process.argv[1],
|
|
899
|
+
workspace: config.workspace,
|
|
900
|
+
runId: mcpContext.runId,
|
|
901
|
+
}
|
|
902
|
+
: undefined,
|
|
875
903
|
);
|
|
876
904
|
}
|
|
877
905
|
|