@engineeros/connector 0.8.1 → 0.8.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/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, and soft-delete project artifacts 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
@@ -24,8 +24,26 @@ import {
24
24
  startConnectionWatchdog,
25
25
  } from "../src/connection.mjs";
26
26
  import { advertisedCapabilities } from "../src/capabilities.mjs";
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" };
27
30
 
28
- const { command, positional, flags } = parseArgs(process.argv.slice(2));
31
+ const { command, positional, flags } = parseConnectorArgs(process.argv.slice(2));
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
+ }
29
47
 
30
48
  if (command === "status") {
31
49
  const config = await loadConfig(flags.workspace || process.cwd());
@@ -79,7 +97,7 @@ if (command === "pair") {
79
97
  token: config.token,
80
98
  };
81
99
  } else {
82
- fail("Use `engineeros-connector pair`, `start`, or `status`.");
100
+ fail("Use `engineeros-connector pair`, `start`, `status`, or `mcp`.");
83
101
  }
84
102
 
85
103
  let codingAgent;
@@ -566,19 +584,6 @@ async function execute(assignment) {
566
584
  }
567
585
  }
568
586
 
569
- function parseArgs(args) {
570
- const command = args.shift();
571
- const positional = [];
572
- const flags = {};
573
- while (args.length) {
574
- const value = args.shift();
575
- if (!value.startsWith("--")) positional.push(value);
576
- else if (value === "--onboard") flags.onboard = true;
577
- else flags[value.slice(2)] = args.shift();
578
- }
579
- return { command, positional, flags };
580
- }
581
-
582
587
  function parseAgentArgs(value) {
583
588
  if (!value) return [];
584
589
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
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/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"
@@ -0,0 +1,18 @@
1
+ const BOOLEAN_FLAGS = new Set(["onboard", "skip-git-repo-check"]);
2
+
3
+ export function parseConnectorArgs(argv) {
4
+ const args = [...argv];
5
+ const command = args.shift();
6
+ const positional = [];
7
+ const flags = {};
8
+ while (args.length) {
9
+ const value = args.shift();
10
+ if (!value.startsWith("--")) {
11
+ positional.push(value);
12
+ continue;
13
+ }
14
+ const name = value.slice(2);
15
+ flags[name] = BOOLEAN_FLAGS.has(name) ? true : args.shift();
16
+ }
17
+ return { command, positional, flags };
18
+ }
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,241 @@
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_delete_artifact",
124
+ description:
125
+ "Soft-delete a saved EngineerOS artifact during the currently approved writable Goal.",
126
+ inputSchema: {
127
+ type: "object",
128
+ properties: { artifact_id: artifactIdSchema },
129
+ required: ["artifact_id"],
130
+ additionalProperties: false,
131
+ },
132
+ annotations: { readOnlyHint: false, destructiveHint: true },
133
+ },
134
+ ];
135
+
136
+ const toolNames = new Map(
137
+ artifactTools.map((tool) => [tool.name, tool.name.replace("engineeros_", "")]),
138
+ );
139
+
140
+ export async function handleMcpRequest(message, context) {
141
+ if (message.method === "notifications/initialized") return null;
142
+ if (message.method === "initialize") {
143
+ return success(message.id, {
144
+ protocolVersion: message.params?.protocolVersion || PROTOCOL_VERSION,
145
+ capabilities: { tools: { listChanged: false } },
146
+ serverInfo: { name: "engineeros-connector", version: context.version },
147
+ });
148
+ }
149
+ if (message.method === "ping") return success(message.id, {});
150
+ if (message.method === "tools/list") {
151
+ return success(message.id, { tools: artifactTools });
152
+ }
153
+ if (message.method === "tools/call") {
154
+ const tool = toolNames.get(message.params?.name);
155
+ if (!tool) return failure(message.id, -32602, "Unknown EngineerOS artifact tool.");
156
+ try {
157
+ const response = await callArtifactTool(context, tool, message.params?.arguments || {});
158
+ return success(message.id, {
159
+ content: [{ type: "text", text: renderArtifactToolResult(response) }],
160
+ isError: false,
161
+ });
162
+ } catch (error) {
163
+ return success(message.id, {
164
+ content: [
165
+ {
166
+ type: "text",
167
+ text: `# EngineerOS artifact tool failed\n\n${error instanceof Error ? error.message : String(error)}`,
168
+ },
169
+ ],
170
+ isError: true,
171
+ });
172
+ }
173
+ }
174
+ return failure(message.id, -32601, `Unsupported MCP method: ${message.method}`);
175
+ }
176
+
177
+ export async function runMcpServer({ config, runId, version, input, output, fetchImpl = fetch }) {
178
+ const context = { config, runId, version, fetchImpl };
179
+ const lines = readline.createInterface({ input, crlfDelay: Infinity });
180
+ for await (const line of lines) {
181
+ if (!line.trim()) continue;
182
+ let response;
183
+ try {
184
+ response = await handleMcpRequest(JSON.parse(line), context);
185
+ } catch (error) {
186
+ response = failure(null, -32700, error instanceof Error ? error.message : String(error));
187
+ }
188
+ if (response) output.write(`${JSON.stringify(response)}\n`);
189
+ }
190
+ }
191
+
192
+ async function callArtifactTool(context, tool, arguments_) {
193
+ const response = await context.fetchImpl(
194
+ artifactToolUrl(context.config.server_url, context.config.connector_id),
195
+ {
196
+ method: "POST",
197
+ headers: {
198
+ "Content-Type": "application/json",
199
+ Authorization: `Bearer ${context.config.token}`,
200
+ },
201
+ body: JSON.stringify({ run_id: context.runId, tool, arguments: arguments_ }),
202
+ },
203
+ );
204
+ if (!response.ok) {
205
+ const detail = await response.text();
206
+ throw new Error(`EngineerOS rejected ${tool} (${response.status}): ${detail}`);
207
+ }
208
+ return response.json();
209
+ }
210
+
211
+ function renderArtifactToolResult(result) {
212
+ const lines = ["# EngineerOS artifact tool result", "", result.message || "Completed."];
213
+ if (result.artifact) lines.push("", ...artifactMarkdown(result.artifact));
214
+ if (Array.isArray(result.artifacts)) {
215
+ lines.push("", `## Artifacts (${result.total ?? result.artifacts.length})`, "");
216
+ for (const artifact of result.artifacts) {
217
+ lines.push(`- **${artifact.name}** — \`${artifact.artifact_type}\` — \`${artifact.id}\``);
218
+ }
219
+ }
220
+ return lines.join("\n");
221
+ }
222
+
223
+ function artifactMarkdown(artifact) {
224
+ return [
225
+ `## ${artifact.name}`,
226
+ "",
227
+ `- ID: \`${artifact.id}\``,
228
+ `- Type: \`${artifact.artifact_type}\``,
229
+ `- Status: \`${artifact.status}\``,
230
+ "",
231
+ artifact.content || "",
232
+ ];
233
+ }
234
+
235
+ function success(id, result) {
236
+ return { jsonrpc: "2.0", id, result };
237
+ }
238
+
239
+ function failure(id, code, message) {
240
+ return { jsonrpc: "2.0", id, error: { code, message } };
241
+ }
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