create-academic-research 0.1.13 → 0.1.15

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.
@@ -7,10 +7,14 @@ export interface McpEnvironmentEntry {
7
7
  export declare function listMcpEnvironmentEntries(servers: string[], options?: {
8
8
  requiredOnly?: boolean;
9
9
  recommendedOnly?: boolean;
10
+ mode?: string;
11
+ modes?: Record<string, string | undefined>;
10
12
  }): McpEnvironmentEntry[];
11
13
  export declare function formatMcpDotenv(servers: string[], options?: {
12
14
  requiredOnly?: boolean;
13
15
  recommendedOnly?: boolean;
16
+ mode?: string;
17
+ modes?: Record<string, string | undefined>;
14
18
  }): string;
15
19
  export declare function readMcpEnvironmentFile(path: string): Promise<Record<string, string>>;
16
20
  export declare function mergeMcpEnvironment(baseEnv?: NodeJS.ProcessEnv, fileEnv?: Record<string, string>): NodeJS.ProcessEnv;
@@ -1,10 +1,10 @@
1
1
  import { readFile } from "node:fs/promises";
2
- import { AGENT_STACK } from "./stack.js";
2
+ import { AGENT_STACK, resolveMcpServer } from "./stack.js";
3
3
  export function listMcpEnvironmentEntries(servers, options = {}) {
4
4
  assertKnownMcpServers(servers);
5
5
  const entries = [];
6
6
  for (const serverName of servers) {
7
- const server = AGENT_STACK.mcp_servers[serverName];
7
+ const server = resolveMcpServer(serverName, options.mode ?? options.modes?.[serverName]);
8
8
  if (!options.recommendedOnly) {
9
9
  for (const envName of server.required_env) {
10
10
  entries.push({ server: serverName, kind: "required", name: envName, value: "" });
@@ -1,4 +1,5 @@
1
- export type McpProbeStatus = "ok" | "manual" | "missing-env" | "runtime-missing" | "startup-failed" | "protocol-error" | "timeout";
1
+ import { type ResolvedMcpServer } from "./stack.js";
2
+ export type McpProbeStatus = "ok" | "manual" | "remote-configured" | "missing-remote-url" | "missing-env" | "runtime-missing" | "startup-failed" | "protocol-error" | "timeout";
2
3
  export interface McpProbeServerResult {
3
4
  server: string;
4
5
  status: McpProbeStatus;
@@ -8,4 +9,4 @@ export interface McpProbeResult {
8
9
  ok: boolean;
9
10
  results: McpProbeServerResult[];
10
11
  }
11
- export declare function probeMcpServerList(root: string, servers: string[], env: NodeJS.ProcessEnv, timeoutMs: number, clientVersion?: string): Promise<McpProbeResult>;
12
+ export declare function probeMcpServerList(root: string, servers: string[], env: NodeJS.ProcessEnv, timeoutMs: number, clientVersion?: string, modes?: Record<string, string>, resolvedServers?: Record<string, ResolvedMcpServer>): Promise<McpProbeResult>;
@@ -1,28 +1,46 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
- import { delimiter, join } from "node:path";
4
- import { AGENT_STACK } from "./stack.js";
5
- export async function probeMcpServerList(root, servers, env, timeoutMs, clientVersion = "unknown") {
3
+ import { delimiter, isAbsolute, join } from "node:path";
4
+ import { AGENT_STACK, resolveMcpServer } from "./stack.js";
5
+ export async function probeMcpServerList(root, servers, env, timeoutMs, clientVersion = "unknown", modes = {}, resolvedServers = {}) {
6
6
  assertKnownMcpServers(servers);
7
7
  const results = [];
8
8
  for (const name of servers) {
9
- const server = AGENT_STACK.mcp_servers[name];
9
+ const server = resolvedServers[name] ?? resolveMcpServer(name, modes[name]);
10
10
  const missingRequired = server.required_env.filter((envName) => !envHasValue(env, envName));
11
11
  if (missingRequired.length > 0) {
12
12
  results.push({ server: name, status: "missing-env", detail: missingRequired.join(",") });
13
13
  continue;
14
14
  }
15
+ if (server.connection_mode === "remote-custom" && !server.remote_configured) {
16
+ results.push({ server: name, status: "missing-remote-url", detail: "custom remote endpoint not configured" });
17
+ continue;
18
+ }
19
+ if (server.connection_mode === "remote-curated" || server.connection_mode === "remote-custom") {
20
+ results.push({ server: name, status: "remote-configured", detail: remoteProbeDetail(server) });
21
+ continue;
22
+ }
15
23
  if (!server.command) {
16
- results.push({ server: name, status: "manual", detail: server.local_service || "manual setup only" });
24
+ results.push({ server: name, status: "manual", detail: server.hosted_url || server.local_service || "manual setup only" });
17
25
  continue;
18
26
  }
19
- if (!commandExists(server.command, env)) {
27
+ const command = resolveCommand(root, server.command);
28
+ if (!commandExists(command, env)) {
20
29
  results.push({ server: name, status: "runtime-missing", detail: server.command });
21
30
  continue;
22
31
  }
23
- results.push(await probeMcpServerProcess(root, name, server.command, server.args, { ...server.env, ...env }, timeoutMs, clientVersion));
32
+ results.push(await probeMcpServerProcess(root, name, command, server.args, { ...server.env, ...env }, timeoutMs, clientVersion));
24
33
  }
25
- return { ok: results.every((result) => result.status === "ok"), results };
34
+ return { ok: results.every((result) => result.status === "ok" || result.status === "remote-configured"), results };
35
+ }
36
+ function remoteProbeDetail(server) {
37
+ const urlEnv = server.connection_mode === "remote-custom" ? server.remote_url_env : undefined;
38
+ const source = urlEnv
39
+ ? `custom remote endpoint from ${urlEnv}`
40
+ : server.connection_mode === "remote-custom"
41
+ ? "custom remote endpoint configured"
42
+ : "remote endpoint configured";
43
+ return `${source}; remote probe does not perform a stdio handshake`;
26
44
  }
27
45
  function assertKnownMcpServers(servers) {
28
46
  const unknown = servers.filter((server) => !AGENT_STACK.mcp_servers[server]);
@@ -52,6 +70,11 @@ function commandExists(command, env = process.env) {
52
70
  }
53
71
  return false;
54
72
  }
73
+ function resolveCommand(root, command) {
74
+ if (!command.includes("/") && !command.includes("\\"))
75
+ return command;
76
+ return isAbsolute(command) ? command : join(root, command);
77
+ }
55
78
  async function probeMcpServerProcess(root, server, command, args, env, timeoutMs, clientVersion) {
56
79
  return new Promise((resolve) => {
57
80
  let settled = false;
@@ -96,7 +119,7 @@ async function probeMcpServerProcess(root, server, command, args, env, timeoutMs
96
119
  }
97
120
  });
98
121
  try {
99
- child.stdin.write(encodeMcpMessage({
122
+ child.stdin.write(encodeMcpLineMessage({
100
123
  jsonrpc: "2.0",
101
124
  id: 1,
102
125
  method: "initialize",
@@ -116,12 +139,12 @@ async function probeMcpServerProcess(root, server, command, args, env, timeoutMs
116
139
  finish("protocol-error", formatJsonRpcError(message.error));
117
140
  return;
118
141
  }
119
- child.stdin.write(encodeMcpMessage({
142
+ child.stdin.write(encodeMcpLineMessage({
120
143
  jsonrpc: "2.0",
121
144
  method: "notifications/initialized",
122
145
  params: {}
123
146
  }));
124
- child.stdin.write(encodeMcpMessage({
147
+ child.stdin.write(encodeMcpLineMessage({
125
148
  jsonrpc: "2.0",
126
149
  id: 2,
127
150
  method: "tools/list",
@@ -143,31 +166,65 @@ async function probeMcpServerProcess(root, server, command, args, env, timeoutMs
143
166
  function drainMcpMessages() {
144
167
  const messages = [];
145
168
  while (true) {
146
- const separator = stdout.indexOf("\r\n\r\n");
147
- if (separator === -1)
169
+ stdout = trimLeadingMcpWhitespace(stdout);
170
+ if (stdout.length === 0)
148
171
  return messages;
149
- const header = stdout.slice(0, separator).toString("utf8");
150
- const match = /Content-Length:\s*(\d+)/i.exec(header);
151
- if (!match)
152
- throw new Error("missing Content-Length header");
153
- const length = Number(match[1]);
154
- const bodyStart = separator + 4;
155
- const bodyEnd = bodyStart + length;
156
- if (stdout.length < bodyEnd)
172
+ if (startsWithContentLength(stdout)) {
173
+ const framed = drainContentLengthMessage(stdout);
174
+ if (!framed)
175
+ return messages;
176
+ stdout = framed.remaining;
177
+ messages.push(parseMcpMessage(framed.body));
178
+ continue;
179
+ }
180
+ const newline = stdout.indexOf("\n");
181
+ if (newline === -1)
157
182
  return messages;
158
- const body = stdout.slice(bodyStart, bodyEnd).toString("utf8");
159
- stdout = stdout.slice(bodyEnd);
160
- const parsed = JSON.parse(body);
161
- if (typeof parsed !== "object" || parsed === null)
162
- throw new Error("MCP response is not an object");
163
- messages.push(parsed);
183
+ const line = stdout.slice(0, newline).toString("utf8").trim();
184
+ stdout = stdout.slice(newline + 1);
185
+ if (!line)
186
+ continue;
187
+ messages.push(parseMcpMessage(line));
164
188
  }
165
189
  }
166
190
  });
167
191
  }
168
- function encodeMcpMessage(message) {
169
- const body = JSON.stringify(message);
170
- return `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
192
+ function encodeMcpLineMessage(message) {
193
+ return `${JSON.stringify(message)}\n`;
194
+ }
195
+ function trimLeadingMcpWhitespace(buffer) {
196
+ let offset = 0;
197
+ while (offset < buffer.length && (buffer[offset] === 0x0a || buffer[offset] === 0x0d)) {
198
+ offset += 1;
199
+ }
200
+ return offset === 0 ? buffer : buffer.slice(offset);
201
+ }
202
+ function startsWithContentLength(buffer) {
203
+ return buffer.slice(0, "Content-Length:".length).toString("utf8").toLowerCase() === "content-length:";
204
+ }
205
+ function drainContentLengthMessage(buffer) {
206
+ const separator = buffer.indexOf("\r\n\r\n");
207
+ if (separator === -1)
208
+ return undefined;
209
+ const header = buffer.slice(0, separator).toString("utf8");
210
+ const match = /Content-Length:\s*(\d+)/i.exec(header);
211
+ if (!match)
212
+ throw new Error("missing Content-Length header");
213
+ const length = Number(match[1]);
214
+ const bodyStart = separator + 4;
215
+ const bodyEnd = bodyStart + length;
216
+ if (buffer.length < bodyEnd)
217
+ return undefined;
218
+ return {
219
+ body: buffer.slice(bodyStart, bodyEnd).toString("utf8"),
220
+ remaining: buffer.slice(bodyEnd)
221
+ };
222
+ }
223
+ function parseMcpMessage(raw) {
224
+ const parsed = JSON.parse(raw);
225
+ if (typeof parsed !== "object" || parsed === null)
226
+ throw new Error("MCP response is not an object");
227
+ return parsed;
171
228
  }
172
229
  function formatJsonRpcError(error) {
173
230
  if (typeof error === "object" && error !== null && "message" in error) {
@@ -13,6 +13,11 @@ export interface RenameProjectOptions {
13
13
  slug?: string;
14
14
  packageName?: string;
15
15
  }
16
+ export interface InitProjectOptions extends CreateProjectOptions {
17
+ }
18
+ export interface UpdateProjectOptions {
19
+ apply?: boolean;
20
+ }
16
21
  export interface ProjectResult {
17
22
  root: string;
18
23
  title: string;
@@ -22,7 +27,20 @@ export interface ProjectResult {
22
27
  export interface DoctorResult {
23
28
  ok: boolean;
24
29
  errors: string[];
30
+ warnings: string[];
31
+ }
32
+ export interface ProjectFileChange {
33
+ path: string;
34
+ action: "create" | "update" | "skip";
35
+ reason?: string;
36
+ }
37
+ export interface UpdateProjectResult {
38
+ root: string;
39
+ applied: boolean;
40
+ changes: ProjectFileChange[];
25
41
  }
26
42
  export declare function createProject(options: CreateProjectOptions): Promise<ProjectResult>;
43
+ export declare function initProject(options: InitProjectOptions): Promise<ProjectResult>;
27
44
  export declare function renameProject(root: string, options: RenameProjectOptions): Promise<ProjectResult>;
45
+ export declare function updateProject(root: string, options?: UpdateProjectOptions): Promise<UpdateProjectResult>;
28
46
  export declare function doctorProject(root: string): Promise<DoctorResult>;