create-academic-research 0.1.8 → 0.1.10
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/CHANGELOG.md +9 -0
- package/README.md +24 -11
- package/dist/src/capabilities.d.ts +16 -1
- package/dist/src/capabilities.js +21 -4
- package/dist/src/cli.js +114 -30
- package/dist/src/mcp-env.d.ts +16 -0
- package/dist/src/mcp-env.js +122 -0
- package/dist/src/mcp-probe.d.ts +11 -0
- package/dist/src/mcp-probe.js +177 -0
- package/dist/src/project.js +16 -4
- package/dist/src/stack.js +1 -1
- package/package.json +2 -1
- package/template/.env.example +23 -0
- package/template/AGENTS.md +2 -0
- package/template/README.md +14 -3
- package/template/docs/agent/mcp-client-setup.md +67 -0
- package/template/docs/agent/mcp-setup.md +9 -0
- package/template/docs/getting-started.md +86 -0
- package/template/package.json +4 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Release notes are generated from merged commits and pull requests in GitHub
|
|
4
|
+
Releases:
|
|
5
|
+
|
|
6
|
+
https://github.com/VincenzoImp/create-academic-research/releases
|
|
7
|
+
|
|
8
|
+
This file intentionally does not duplicate release entries, so the repository
|
|
9
|
+
has one authoritative changelog source.
|
package/README.md
CHANGED
|
@@ -104,12 +104,15 @@ npx academic-research mcp enabled
|
|
|
104
104
|
npx academic-research mcp available
|
|
105
105
|
npx academic-research mcp commands arxiv
|
|
106
106
|
npx academic-research mcp env openalex semantic-scholar zotero
|
|
107
|
+
npx academic-research mcp env --dotenv --all > .env.example
|
|
108
|
+
npx academic-research mcp env --write .env.example --all
|
|
107
109
|
npx academic-research mcp enable arxiv dblp
|
|
108
110
|
npx academic-research mcp disable arxiv
|
|
109
111
|
npx academic-research mcp install arxiv
|
|
110
112
|
npx academic-research mcp uninstall arxiv
|
|
111
|
-
npx academic-research mcp smoke
|
|
112
|
-
npx academic-research mcp doctor
|
|
113
|
+
npx academic-research mcp smoke --env-file .env.local
|
|
114
|
+
npx academic-research mcp doctor --env-file .env.local
|
|
115
|
+
npx academic-research mcp probe arxiv --timeout-ms 5000
|
|
113
116
|
```
|
|
114
117
|
|
|
115
118
|
## Command Model
|
|
@@ -137,13 +140,14 @@ MCP commands are split by side-effect:
|
|
|
137
140
|
| `mcp enabled` | List only enabled MCP server ids. |
|
|
138
141
|
| `mcp available` | List the local MCP catalog. |
|
|
139
142
|
| `mcp commands` | Print finite external install commands without running them. Runtime-only `uvx`/`npx` servers may have no install command. |
|
|
140
|
-
| `mcp env` | Print required/recommended env vars, hosted endpoints, local prerequisites, and setup commands for selected servers. |
|
|
143
|
+
| `mcp env` | Print required/recommended env vars, hosted endpoints, local prerequisites, and setup commands for selected servers. Use `--dotenv --all` to print dotenv content or `--write .env.example --all` to regenerate `.env.example`. |
|
|
141
144
|
| `mcp enable` | Enable an MCP server in project records and generated snippets. |
|
|
142
145
|
| `mcp disable` | Remove an MCP server from project records and generated snippets. |
|
|
143
146
|
| `mcp install` | Run finite external tool install commands for selected MCP servers. It must not launch stdio MCP servers. |
|
|
144
147
|
| `mcp uninstall` | Run the external uninstall command when one exists. |
|
|
145
148
|
| `mcp smoke` | Print non-launching readiness diagnostics for enabled or selected MCP servers. |
|
|
146
|
-
| `mcp doctor` | Validate enabled MCP records, generated snippets, required env vars, and documented manual prerequisites. |
|
|
149
|
+
| `mcp doctor` | Validate enabled MCP records, generated snippets, required env vars, and documented manual prerequisites. Pass `--env-file .env.local` to read explicit local secrets. |
|
|
150
|
+
| `mcp probe` | Opt-in runtime check that starts selected MCP servers and performs a stdio JSON-RPC handshake. |
|
|
147
151
|
|
|
148
152
|
## Companion Skills
|
|
149
153
|
|
|
@@ -185,11 +189,18 @@ The MCP catalog distinguishes local runtime adapters from hosted endpoints and
|
|
|
185
189
|
manual integrations. arXiv and DBLP are low-friction local `uvx` runtimes.
|
|
186
190
|
Semantic Scholar is useful for citation graphs but works best with
|
|
187
191
|
`SEMANTIC_SCHOLAR_API_KEY`. OpenAlex requires `OPENALEX_API_KEY` for the
|
|
188
|
-
selected local adapter; OpenAlex keys are free
|
|
189
|
-
|
|
190
|
-
opt-in. Zotero needs the local
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
selected local adapter; OpenAlex keys are free and include a free daily quota,
|
|
193
|
+
but high-volume work should check current credit limits. PubMed is a
|
|
194
|
+
biomedical-specific `npx` runtime and remains opt-in. Zotero needs the local
|
|
195
|
+
Zotero desktop app and Zoty setup. Overleaf is manual and credentialed.
|
|
196
|
+
Crossref and broad paper-search aggregators are kept as fallback/manual entries
|
|
197
|
+
until a project explicitly needs them.
|
|
198
|
+
|
|
199
|
+
Generated projects include a committed `.env.example` with empty MCP variables
|
|
200
|
+
and ignore filled `.env` or `.env.local` files. Regenerate the example with
|
|
201
|
+
`mcp env --write .env.example --all`. `mcp doctor`, `mcp smoke`, and
|
|
202
|
+
`mcp probe` check the current process environment unless you explicitly pass
|
|
203
|
+
`--env-file .env.local`.
|
|
193
204
|
|
|
194
205
|
Generated MCP snippets are project documentation and client-ready config, not
|
|
195
206
|
live tools by themselves. Your MCP client must load the generated snippet, and
|
|
@@ -199,6 +210,8 @@ tool install; it deliberately does not launch stdio MCP servers.
|
|
|
199
210
|
Use `mcp smoke` for a non-launching readiness pass before wiring a client: it
|
|
200
211
|
checks required env vars, manual/local-service status, and whether runtime
|
|
201
212
|
commands are visible on `PATH`.
|
|
213
|
+
Use `mcp probe` only when you intentionally want to start selected MCP server
|
|
214
|
+
processes and verify a real stdio JSON-RPC handshake.
|
|
202
215
|
|
|
203
216
|
## Validate This Package
|
|
204
217
|
|
|
@@ -216,8 +229,8 @@ Releases are tag-driven. Update `package.json` and `package-lock.json`, commit
|
|
|
216
229
|
the change, create `vX.Y.Z`, and push the tag:
|
|
217
230
|
|
|
218
231
|
```bash
|
|
219
|
-
git tag -a v0.1.
|
|
220
|
-
git push origin main v0.1.
|
|
232
|
+
git tag -a v0.1.10 -m "v0.1.10"
|
|
233
|
+
git push origin main v0.1.10
|
|
221
234
|
```
|
|
222
235
|
|
|
223
236
|
Once the GitHub repository is public, the release workflow validates the tag
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { DEFAULT_AGENT, SUPPORTED_SKILL_AGENT_TARGETS } from "./agents.js";
|
|
2
2
|
import { type Runner } from "./runner.js";
|
|
3
3
|
import { type McpToolCommandKey } from "./stack.js";
|
|
4
|
+
import { formatMcpDotenv, listMcpEnvironmentEntries } from "./mcp-env.js";
|
|
5
|
+
import { type McpProbeResult as ProbeResult } from "./mcp-probe.js";
|
|
6
|
+
export { formatMcpDotenv, listMcpEnvironmentEntries };
|
|
7
|
+
export { mergeMcpEnvironment, readMcpEnvironmentFile, type McpEnvironmentEntry } from "./mcp-env.js";
|
|
8
|
+
export { type McpProbeResult, type McpProbeServerResult, type McpProbeStatus } from "./mcp-probe.js";
|
|
4
9
|
export { DEFAULT_AGENT, SUPPORTED_SKILL_AGENT_TARGETS };
|
|
5
10
|
export interface CapabilityState {
|
|
6
11
|
agent: string;
|
|
@@ -30,11 +35,20 @@ export interface McpDoctorResult {
|
|
|
30
35
|
warnings: string[];
|
|
31
36
|
enabled: string[];
|
|
32
37
|
}
|
|
38
|
+
export interface McpDoctorOptions {
|
|
39
|
+
env?: NodeJS.ProcessEnv;
|
|
40
|
+
}
|
|
41
|
+
export interface McpProbeOptions {
|
|
42
|
+
env?: NodeJS.ProcessEnv;
|
|
43
|
+
timeoutMs?: number;
|
|
44
|
+
clientVersion?: string;
|
|
45
|
+
}
|
|
33
46
|
interface SkillInstallOptions {
|
|
34
47
|
agent?: string;
|
|
35
48
|
}
|
|
36
49
|
export declare function readCapabilities(root: string): Promise<CapabilityState>;
|
|
37
50
|
export declare function writeCapabilities(root: string, state: Partial<CapabilityState>): Promise<void>;
|
|
51
|
+
export declare function writeMcpEnvironmentExample(root: string): Promise<void>;
|
|
38
52
|
export declare function initializeCapabilities(root: string, options?: InitializeCapabilitiesOptions): Promise<void>;
|
|
39
53
|
export declare function buildSkillInstallCommands(root: string, preset?: string, options?: SkillInstallOptions): Promise<string[][]>;
|
|
40
54
|
export declare function buildExplicitSkillInstallCommands(root: string, skills: string[], options?: SkillInstallOptions): Promise<string[][]>;
|
|
@@ -53,5 +67,6 @@ export declare function mcpToolCommands(servers: string[], key?: McpToolCommandK
|
|
|
53
67
|
export declare function mcpToolCommandTexts(servers: string[], key?: McpToolCommandKey): string[];
|
|
54
68
|
export declare function installMcpTools(root: string, servers: string[], runner?: Runner): Promise<CapabilityCommandResult>;
|
|
55
69
|
export declare function uninstallMcpTools(root: string, servers: string[], runner?: Runner): Promise<CapabilityCommandResult>;
|
|
56
|
-
export declare function doctorMcpServers(root: string): Promise<McpDoctorResult>;
|
|
70
|
+
export declare function doctorMcpServers(root: string, options?: McpDoctorOptions): Promise<McpDoctorResult>;
|
|
71
|
+
export declare function probeMcpServers(root: string, servers: string[], options?: McpProbeOptions): Promise<ProbeResult>;
|
|
57
72
|
export declare function assertKnownMcpServers(servers: string[]): void;
|
package/dist/src/capabilities.js
CHANGED
|
@@ -4,6 +4,10 @@ import YAML from "yaml";
|
|
|
4
4
|
import { assertKnownAgentTarget, AUTO_AGENT, DEFAULT_AGENT, normalizeAgentTarget, SUPPORTED_SKILL_AGENT_TARGETS } from "./agents.js";
|
|
5
5
|
import { defaultRunner } from "./runner.js";
|
|
6
6
|
import { AGENT_STACK, presetMcpServers } from "./stack.js";
|
|
7
|
+
import { formatMcpDotenv, listMcpEnvironmentEntries } from "./mcp-env.js";
|
|
8
|
+
import { probeMcpServerList } from "./mcp-probe.js";
|
|
9
|
+
export { formatMcpDotenv, listMcpEnvironmentEntries };
|
|
10
|
+
export { mergeMcpEnvironment, readMcpEnvironmentFile } from "./mcp-env.js";
|
|
7
11
|
export { DEFAULT_AGENT, SUPPORTED_SKILL_AGENT_TARGETS };
|
|
8
12
|
export async function readCapabilities(root) {
|
|
9
13
|
try {
|
|
@@ -29,6 +33,9 @@ export async function writeCapabilities(root, state) {
|
|
|
29
33
|
await writeMcpSnippet(root, next);
|
|
30
34
|
await appendCapabilityLog(root, next);
|
|
31
35
|
}
|
|
36
|
+
export async function writeMcpEnvironmentExample(root) {
|
|
37
|
+
await writeFile(join(root, ".env.example"), formatMcpDotenv(Object.keys(AGENT_STACK.mcp_servers)), "utf8");
|
|
38
|
+
}
|
|
32
39
|
export async function initializeCapabilities(root, options = {}) {
|
|
33
40
|
const preset = options.preset ?? "default";
|
|
34
41
|
const mcpServers = options.mcpServers ?? presetMcpServers(preset);
|
|
@@ -222,9 +229,10 @@ export async function uninstallMcpTools(root, servers, runner = defaultRunner) {
|
|
|
222
229
|
}
|
|
223
230
|
return { ok: true, count: commands.length };
|
|
224
231
|
}
|
|
225
|
-
export async function doctorMcpServers(root) {
|
|
232
|
+
export async function doctorMcpServers(root, options = {}) {
|
|
226
233
|
const errors = [];
|
|
227
234
|
const warnings = [];
|
|
235
|
+
const env = options.env ?? process.env;
|
|
228
236
|
let state;
|
|
229
237
|
try {
|
|
230
238
|
state = await readCapabilitiesFile(root);
|
|
@@ -266,12 +274,12 @@ export async function doctorMcpServers(root) {
|
|
|
266
274
|
if (!server)
|
|
267
275
|
continue;
|
|
268
276
|
for (const envName of server.required_env) {
|
|
269
|
-
if (!
|
|
277
|
+
if (!envHasValue(env, envName)) {
|
|
270
278
|
errors.push(`${name}: missing required environment variable: ${envName}`);
|
|
271
279
|
}
|
|
272
280
|
}
|
|
273
281
|
for (const envName of server.recommended_env) {
|
|
274
|
-
if (!
|
|
282
|
+
if (!envHasValue(env, envName)) {
|
|
275
283
|
warnings.push(`${name}: recommended environment variable not set: ${envName}`);
|
|
276
284
|
}
|
|
277
285
|
}
|
|
@@ -288,6 +296,12 @@ export async function doctorMcpServers(root) {
|
|
|
288
296
|
}
|
|
289
297
|
return { ok: errors.length === 0, errors, warnings, enabled };
|
|
290
298
|
}
|
|
299
|
+
export async function probeMcpServers(root, servers, options = {}) {
|
|
300
|
+
const selected = servers.length > 0 ? servers : (await readCapabilities(root)).mcp_servers ?? [];
|
|
301
|
+
const env = options.env ?? process.env;
|
|
302
|
+
const timeoutMs = options.timeoutMs ?? 5000;
|
|
303
|
+
return probeMcpServerList(root, selected, env, timeoutMs, options.clientVersion);
|
|
304
|
+
}
|
|
291
305
|
async function writeMcpSnippet(root, state) {
|
|
292
306
|
const servers = {};
|
|
293
307
|
for (const name of state.mcp_servers ?? []) {
|
|
@@ -369,7 +383,7 @@ async function writeMcpSetup(root, state) {
|
|
|
369
383
|
lines.push(`- \`${name}\` (${status}, ${server.readiness}, ${server.priority}): ${server.source_need}`, ` - Source: \`${server.source}\``, ` - Execution mode: \`${server.execution_mode}\``, ...(server.hosted_url ? [` - Hosted endpoint: <${server.hosted_url}>`] : []), ...server.setup_commands.map((command) => ` - Setup command: \`${command}\``));
|
|
370
384
|
appendMcpPrerequisiteLines(lines, server.required_env, server.recommended_env, server.local_service);
|
|
371
385
|
}
|
|
372
|
-
lines.push("", "## Operating Rules", "", "- Keep secrets in your shell, MCP client secret store, or local untracked files; do not commit tokens or API keys.", "- Prefer the smallest enabled MCP set that covers the current research question.", "- Treat MCP output as retrieval metadata. Promote claims into repository source records only after source ingestion and citation audit.", "- Run `npx academic-research mcp doctor` after changing MCP records or environment variables.", "");
|
|
386
|
+
lines.push("", "## Operating Rules", "", "- Use `.env.example` as a committed reference and put filled secrets in `.env.local`, your shell, or your MCP client secret store.", "- Print a dotenv-style reference with `npx academic-research mcp env --dotenv --all`.", "- Regenerate a dotenv-style reference with `npx academic-research mcp env --write .env.example --all`.", "- Pass `--env-file .env.local` to `mcp doctor`, `mcp smoke`, or `mcp probe` when you want the CLI to read explicit local secrets.", "- Keep secrets in your shell, MCP client secret store, or local untracked files; do not commit tokens or API keys.", "- Prefer the smallest enabled MCP set that covers the current research question.", "- Treat MCP output as retrieval metadata. Promote claims into repository source records only after source ingestion and citation audit.", "- Run `npx academic-research mcp doctor` after changing MCP records or environment variables.", "- Run `npx academic-research mcp probe <server>` only when you intentionally want to start selected MCP server processes.", "");
|
|
373
387
|
await mkdir(join(root, "docs/agent"), { recursive: true });
|
|
374
388
|
await writeFile(join(root, "docs/agent/mcp-setup.md"), lines.join("\n"), "utf8");
|
|
375
389
|
}
|
|
@@ -382,6 +396,9 @@ async function appendCapabilityLog(root, state) {
|
|
|
382
396
|
function dedupe(values) {
|
|
383
397
|
return [...new Set(values)];
|
|
384
398
|
}
|
|
399
|
+
function envHasValue(env, name) {
|
|
400
|
+
return typeof env[name] === "string" && env[name] !== "";
|
|
401
|
+
}
|
|
385
402
|
function renderSkillCommand(command, agent) {
|
|
386
403
|
const normalized = assertKnownAgentTarget(agent);
|
|
387
404
|
const agentFlag = normalized === AUTO_AGENT ? "" : `--agent '${normalized}'`;
|
package/dist/src/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { assertKnownMcpServers, disableMcpServers, doctorMcpServers, enableMcpServers, DEFAULT_AGENT, installMcpTools, installSkillIds, installSkills, listInstalledSkills, mcpToolCommandTexts, readCapabilities, removeSkills, uninstallMcpTools, updateSkills } from "./capabilities.js";
|
|
4
|
+
import { assertKnownMcpServers, disableMcpServers, doctorMcpServers, enableMcpServers, DEFAULT_AGENT, installMcpTools, installSkillIds, installSkills, formatMcpDotenv, listInstalledSkills, listMcpEnvironmentEntries, mergeMcpEnvironment, mcpToolCommandTexts, probeMcpServers, readCapabilities, readMcpEnvironmentFile, removeSkills, uninstallMcpTools, updateSkills } from "./capabilities.js";
|
|
5
5
|
import { createProject, doctorProject, renameProject } from "./project.js";
|
|
6
6
|
import { askCreateOptions } from "./prompts.js";
|
|
7
7
|
import { AGENT_STACK, presetMcpServers } from "./stack.js";
|
|
@@ -21,7 +21,7 @@ const CREATE_FLAGS = flagSchema([
|
|
|
21
21
|
const ROOT_FLAGS = flagSchema(["help"], ["root"]);
|
|
22
22
|
const RENAME_FLAGS = flagSchema(["help"], ["root", "title", "slug", "package"]);
|
|
23
23
|
const SKILLS_FLAGS = flagSchema(["help"], ["root", "preset", "agent"]);
|
|
24
|
-
const MCP_FLAGS = flagSchema(["help"], ["root", "agent"]);
|
|
24
|
+
const MCP_FLAGS = flagSchema(["help", "all", "dotenv", "required", "recommended"], ["root", "agent", "env-file", "write", "timeout-ms"]);
|
|
25
25
|
export async function main(argv = process.argv.slice(2), mode = "create") {
|
|
26
26
|
try {
|
|
27
27
|
if (mode === "create")
|
|
@@ -171,7 +171,9 @@ async function setupCommand(argv) {
|
|
|
171
171
|
console.log("academic-research skills status");
|
|
172
172
|
console.log("academic-research mcp list");
|
|
173
173
|
console.log("academic-research mcp env");
|
|
174
|
+
console.log("academic-research mcp env --write .env.example --all");
|
|
174
175
|
console.log("academic-research mcp smoke");
|
|
176
|
+
console.log("academic-research mcp probe arxiv");
|
|
175
177
|
console.log("academic-research doctor");
|
|
176
178
|
return project.ok ? 0 : 1;
|
|
177
179
|
}
|
|
@@ -339,12 +341,34 @@ async function mcpCommand(argv) {
|
|
|
339
341
|
return 0;
|
|
340
342
|
}
|
|
341
343
|
if (subcommand === "env") {
|
|
342
|
-
assertOnlyOptions(parsed.flags, "mcp env", ["root"]);
|
|
344
|
+
assertOnlyOptions(parsed.flags, "mcp env", ["root", "all", "dotenv", "required", "recommended", "write"]);
|
|
343
345
|
const root = resolve(flagString(parsed.flags, "root") ?? ".");
|
|
344
|
-
|
|
346
|
+
if (flagBool(parsed.flags, "required") && flagBool(parsed.flags, "recommended")) {
|
|
347
|
+
throw new Error("mcp env cannot use --required and --recommended together");
|
|
348
|
+
}
|
|
349
|
+
const selected = flagBool(parsed.flags, "all")
|
|
350
|
+
? Object.keys(AGENT_STACK.mcp_servers)
|
|
351
|
+
: parsed.positionals.length > 0
|
|
352
|
+
? parsed.positionals
|
|
353
|
+
: (await readCapabilities(root)).mcp_servers;
|
|
345
354
|
assertKnownMcpServers(selected);
|
|
355
|
+
const filters = {
|
|
356
|
+
requiredOnly: flagBool(parsed.flags, "required"),
|
|
357
|
+
recommendedOnly: flagBool(parsed.flags, "recommended")
|
|
358
|
+
};
|
|
359
|
+
const writePath = flagString(parsed.flags, "write");
|
|
360
|
+
if (writePath) {
|
|
361
|
+
const outputPath = resolve(root, writePath);
|
|
362
|
+
writeFileSync(outputPath, formatMcpDotenv(selected, filters), "utf8");
|
|
363
|
+
console.log(`Wrote MCP dotenv environment reference: ${outputPath}`);
|
|
364
|
+
return 0;
|
|
365
|
+
}
|
|
366
|
+
if (flagBool(parsed.flags, "dotenv")) {
|
|
367
|
+
process.stdout.write(formatMcpDotenv(selected, filters));
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
346
370
|
console.log("id\ttype\tvalue");
|
|
347
|
-
printMcpEnvironment(selected);
|
|
371
|
+
printMcpEnvironment(selected, filters);
|
|
348
372
|
return 0;
|
|
349
373
|
}
|
|
350
374
|
if (subcommand === "enable") {
|
|
@@ -378,15 +402,16 @@ async function mcpCommand(argv) {
|
|
|
378
402
|
return 0;
|
|
379
403
|
}
|
|
380
404
|
if (subcommand === "smoke") {
|
|
381
|
-
assertOnlyOptions(parsed.flags, "mcp smoke", ["root"]);
|
|
405
|
+
assertOnlyOptions(parsed.flags, "mcp smoke", ["root", "env-file"]);
|
|
382
406
|
const root = resolve(flagString(parsed.flags, "root") ?? ".");
|
|
407
|
+
const env = await mcpCommandEnvironment(root, parsed.flags);
|
|
383
408
|
const state = await readCapabilities(root);
|
|
384
409
|
const explicitSelection = parsed.positionals.length > 0;
|
|
385
410
|
const selected = explicitSelection ? parsed.positionals : state.mcp_servers;
|
|
386
411
|
assertKnownMcpServers(selected);
|
|
387
|
-
const failed = printMcpSmokeDiagnostics(selected);
|
|
412
|
+
const failed = printMcpSmokeDiagnostics(selected, env);
|
|
388
413
|
if (!explicitSelection) {
|
|
389
|
-
const result = await doctorMcpServers(root);
|
|
414
|
+
const result = await doctorMcpServers(root, { env });
|
|
390
415
|
for (const error of result.errors)
|
|
391
416
|
console.error(`ERROR: ${error}`);
|
|
392
417
|
for (const warning of result.warnings)
|
|
@@ -396,10 +421,11 @@ async function mcpCommand(argv) {
|
|
|
396
421
|
return failed ? 1 : 0;
|
|
397
422
|
}
|
|
398
423
|
if (subcommand === "doctor") {
|
|
399
|
-
assertOnlyOptions(parsed.flags, "mcp doctor", ["root"]);
|
|
424
|
+
assertOnlyOptions(parsed.flags, "mcp doctor", ["root", "env-file"]);
|
|
400
425
|
const root = resolve(flagString(parsed.flags, "root") ?? ".");
|
|
401
426
|
assertNoArguments(parsed.positionals, "mcp doctor");
|
|
402
|
-
const
|
|
427
|
+
const env = await mcpCommandEnvironment(root, parsed.flags);
|
|
428
|
+
const result = await doctorMcpServers(root, { env });
|
|
403
429
|
for (const error of result.errors)
|
|
404
430
|
console.error(`ERROR: ${error}`);
|
|
405
431
|
for (const warning of result.warnings)
|
|
@@ -408,6 +434,23 @@ async function mcpCommand(argv) {
|
|
|
408
434
|
console.log(`OK: ${result.enabled.length} MCP server(s) enabled.`);
|
|
409
435
|
return result.ok ? 0 : 1;
|
|
410
436
|
}
|
|
437
|
+
if (subcommand === "probe") {
|
|
438
|
+
assertOnlyOptions(parsed.flags, "mcp probe", ["root", "all", "env-file", "timeout-ms"]);
|
|
439
|
+
const root = resolve(flagString(parsed.flags, "root") ?? ".");
|
|
440
|
+
const selected = flagBool(parsed.flags, "all")
|
|
441
|
+
? Object.keys(AGENT_STACK.mcp_servers)
|
|
442
|
+
: parsed.positionals.length > 0
|
|
443
|
+
? parsed.positionals
|
|
444
|
+
: (await readCapabilities(root)).mcp_servers;
|
|
445
|
+
assertKnownMcpServers(selected);
|
|
446
|
+
const timeoutMs = parseTimeoutMs(flagString(parsed.flags, "timeout-ms"));
|
|
447
|
+
const env = await mcpCommandEnvironment(root, parsed.flags);
|
|
448
|
+
const result = await probeMcpServers(root, selected, { env, timeoutMs, clientVersion: packageVersion });
|
|
449
|
+
console.log("id\tstatus\tdetail");
|
|
450
|
+
for (const item of result.results)
|
|
451
|
+
console.log(`${item.server}\t${item.status}\t${item.detail}`);
|
|
452
|
+
return result.ok ? 0 : 1;
|
|
453
|
+
}
|
|
411
454
|
throw new Error(`unknown mcp command: ${subcommand}`);
|
|
412
455
|
}
|
|
413
456
|
function parseFlags(argv, schema) {
|
|
@@ -492,6 +535,24 @@ function assertOnlyOptions(flags, command, allowedOptions) {
|
|
|
492
535
|
throw new Error(`${command} does not accept ${unexpected.map((name) => `--${name}`).join(", ")}`);
|
|
493
536
|
}
|
|
494
537
|
}
|
|
538
|
+
async function mcpCommandEnvironment(root, flags) {
|
|
539
|
+
const envFile = flagString(flags, "env-file");
|
|
540
|
+
if (!envFile)
|
|
541
|
+
return process.env;
|
|
542
|
+
const fileEnv = await readMcpEnvironmentFile(resolve(root, envFile));
|
|
543
|
+
return mergeMcpEnvironment(process.env, fileEnv);
|
|
544
|
+
}
|
|
545
|
+
function parseTimeoutMs(value) {
|
|
546
|
+
if (value === undefined)
|
|
547
|
+
return 5000;
|
|
548
|
+
if (!/^[0-9]+$/.test(value))
|
|
549
|
+
throw new Error(`--timeout-ms must be a positive integer, got: ${value}`);
|
|
550
|
+
const timeoutMs = Number(value);
|
|
551
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 100 || timeoutMs > 120000) {
|
|
552
|
+
throw new Error("--timeout-ms must be between 100 and 120000");
|
|
553
|
+
}
|
|
554
|
+
return timeoutMs;
|
|
555
|
+
}
|
|
495
556
|
export function formatInteractiveCreateGuide() {
|
|
496
557
|
const presetLines = Object.entries(AGENT_STACK.presets).map(([name, preset]) => ` ${name.padEnd(10)} ${preset.description}`);
|
|
497
558
|
return [
|
|
@@ -519,6 +580,9 @@ export function formatInteractiveCreateGuide() {
|
|
|
519
580
|
" MCP installers are optional and run only finite installer commands.",
|
|
520
581
|
" MCP execution modes are explicit: uvx-runtime, npx-runtime, local-service, manual, or fallback.",
|
|
521
582
|
" Use `academic-research mcp env <server>` to inspect env vars and local prerequisites.",
|
|
583
|
+
" Use `academic-research mcp env --dotenv --all` to print a committed env example.",
|
|
584
|
+
" Use `academic-research mcp env --write .env.example --all` to regenerate a committed env example.",
|
|
585
|
+
" Use `academic-research mcp doctor --env-file .env.local` to check explicit local secrets.",
|
|
522
586
|
""
|
|
523
587
|
].join("\n");
|
|
524
588
|
}
|
|
@@ -609,22 +673,37 @@ function printSkillsHelp() {
|
|
|
609
673
|
}
|
|
610
674
|
function printMcpHelp() {
|
|
611
675
|
console.log([
|
|
612
|
-
"Usage: academic-research mcp <list|enabled|available|commands|env|enable|disable|install|uninstall|smoke|doctor> [servers...]",
|
|
676
|
+
"Usage: academic-research mcp <list|enabled|available|commands|env|enable|disable|install|uninstall|smoke|doctor|probe> [servers...]",
|
|
613
677
|
"",
|
|
614
678
|
"Manage MCP records, readiness checks, and finite external MCP tool installs.",
|
|
615
679
|
"",
|
|
680
|
+
"Examples:",
|
|
681
|
+
" academic-research mcp env openalex semantic-scholar",
|
|
682
|
+
" academic-research mcp env --dotenv --all > .env.example",
|
|
683
|
+
" academic-research mcp env --write .env.example --all",
|
|
684
|
+
" academic-research mcp doctor --env-file .env.local",
|
|
685
|
+
" academic-research mcp smoke",
|
|
686
|
+
" academic-research mcp probe arxiv --timeout-ms 5000",
|
|
687
|
+
"",
|
|
616
688
|
"Options:",
|
|
617
689
|
" --root <path> Project root for project-state commands.",
|
|
618
690
|
" --agent <id> Agent for enable/disable generated snippets.",
|
|
691
|
+
" --all Select all catalog MCP servers for mcp env.",
|
|
692
|
+
" --dotenv Print mcp env as dotenv content.",
|
|
693
|
+
" --write <path> Write mcp env dotenv content to a file.",
|
|
694
|
+
" --env-file <path> Read local env values for mcp smoke, doctor, and probe.",
|
|
695
|
+
" --timeout-ms <ms> Per-server probe timeout. Default: 5000.",
|
|
696
|
+
" --required Print only required env vars for mcp env.",
|
|
697
|
+
" --recommended Print only recommended/default env vars for mcp env.",
|
|
619
698
|
" -h, --help Show this help."
|
|
620
699
|
].join("\n"));
|
|
621
700
|
}
|
|
622
|
-
function printMcpSmokeDiagnostics(servers) {
|
|
701
|
+
function printMcpSmokeDiagnostics(servers, env = process.env) {
|
|
623
702
|
let failed = false;
|
|
624
703
|
console.log("id\tstatus\truntime\tcheck");
|
|
625
704
|
for (const name of servers) {
|
|
626
705
|
const server = AGENT_STACK.mcp_servers[name];
|
|
627
|
-
const missingRequired = server.required_env.filter((envName) => !
|
|
706
|
+
const missingRequired = server.required_env.filter((envName) => !env[envName]);
|
|
628
707
|
if (missingRequired.length > 0)
|
|
629
708
|
failed = true;
|
|
630
709
|
const runtime = server.command ? [server.command, ...server.args].join(" ") : "manual setup";
|
|
@@ -632,7 +711,7 @@ function printMcpSmokeDiagnostics(servers) {
|
|
|
632
711
|
if (missingRequired.length > 0) {
|
|
633
712
|
status = `missing-required-env:${missingRequired.join(",")}`;
|
|
634
713
|
}
|
|
635
|
-
else if (server.command && commandExists(server.command)) {
|
|
714
|
+
else if (server.command && commandExists(server.command, env)) {
|
|
636
715
|
status = "runtime-found";
|
|
637
716
|
}
|
|
638
717
|
else if (server.command) {
|
|
@@ -645,42 +724,47 @@ function printMcpSmokeDiagnostics(servers) {
|
|
|
645
724
|
}
|
|
646
725
|
return failed;
|
|
647
726
|
}
|
|
648
|
-
function printMcpEnvironment(servers) {
|
|
727
|
+
function printMcpEnvironment(servers, options = {}) {
|
|
728
|
+
const grouped = new Map();
|
|
729
|
+
for (const entry of listMcpEnvironmentEntries(servers, options)) {
|
|
730
|
+
const entries = grouped.get(entry.server) ?? [];
|
|
731
|
+
entries.push(entry);
|
|
732
|
+
grouped.set(entry.server, entries);
|
|
733
|
+
}
|
|
649
734
|
for (const name of servers) {
|
|
650
735
|
const server = AGENT_STACK.mcp_servers[name];
|
|
736
|
+
const entries = grouped.get(name) ?? [];
|
|
651
737
|
let wroteLine = false;
|
|
652
|
-
for (const
|
|
653
|
-
console.log(`${name}\
|
|
654
|
-
wroteLine = true;
|
|
655
|
-
}
|
|
656
|
-
for (const envName of server.recommended_env) {
|
|
657
|
-
console.log(`${name}\trecommended\t${envName}`);
|
|
738
|
+
for (const entry of entries) {
|
|
739
|
+
console.log(`${name}\t${entry.kind}\t${entry.name}${entry.value ? `=${entry.value}` : ""}`);
|
|
658
740
|
wroteLine = true;
|
|
659
741
|
}
|
|
660
|
-
if (server.hosted_url) {
|
|
742
|
+
if (!options.requiredOnly && !options.recommendedOnly && server.hosted_url) {
|
|
661
743
|
console.log(`${name}\thosted-endpoint\t${server.hosted_url}`);
|
|
662
744
|
wroteLine = true;
|
|
663
745
|
}
|
|
664
|
-
if (server.local_service) {
|
|
746
|
+
if (!options.requiredOnly && !options.recommendedOnly && server.local_service) {
|
|
665
747
|
console.log(`${name}\tlocal-service\t${server.local_service}`);
|
|
666
748
|
wroteLine = true;
|
|
667
749
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
750
|
+
if (!options.requiredOnly && !options.recommendedOnly) {
|
|
751
|
+
for (const command of server.setup_commands) {
|
|
752
|
+
console.log(`${name}\tsetup-command\t${command}`);
|
|
753
|
+
wroteLine = true;
|
|
754
|
+
}
|
|
671
755
|
}
|
|
672
756
|
if (!wroteLine)
|
|
673
757
|
console.log(`${name}\tnone\t-`);
|
|
674
758
|
}
|
|
675
759
|
}
|
|
676
|
-
function commandExists(command) {
|
|
760
|
+
function commandExists(command, env = process.env) {
|
|
677
761
|
if (!command)
|
|
678
762
|
return false;
|
|
679
763
|
if (command.includes("/") || command.includes("\\"))
|
|
680
764
|
return existsSync(command);
|
|
681
|
-
const pathValue =
|
|
765
|
+
const pathValue = env.PATH ?? "";
|
|
682
766
|
const extensions = process.platform === "win32"
|
|
683
|
-
? (
|
|
767
|
+
? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")
|
|
684
768
|
: [""];
|
|
685
769
|
for (const directory of pathValue.split(delimiter).filter(Boolean)) {
|
|
686
770
|
for (const extension of extensions) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface McpEnvironmentEntry {
|
|
2
|
+
server: string;
|
|
3
|
+
kind: "required" | "recommended" | "default";
|
|
4
|
+
name: string;
|
|
5
|
+
value: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function listMcpEnvironmentEntries(servers: string[], options?: {
|
|
8
|
+
requiredOnly?: boolean;
|
|
9
|
+
recommendedOnly?: boolean;
|
|
10
|
+
}): McpEnvironmentEntry[];
|
|
11
|
+
export declare function formatMcpDotenv(servers: string[], options?: {
|
|
12
|
+
requiredOnly?: boolean;
|
|
13
|
+
recommendedOnly?: boolean;
|
|
14
|
+
}): string;
|
|
15
|
+
export declare function readMcpEnvironmentFile(path: string): Promise<Record<string, string>>;
|
|
16
|
+
export declare function mergeMcpEnvironment(baseEnv?: NodeJS.ProcessEnv, fileEnv?: Record<string, string>): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { AGENT_STACK } from "./stack.js";
|
|
3
|
+
export function listMcpEnvironmentEntries(servers, options = {}) {
|
|
4
|
+
assertKnownMcpServers(servers);
|
|
5
|
+
const entries = [];
|
|
6
|
+
for (const serverName of servers) {
|
|
7
|
+
const server = AGENT_STACK.mcp_servers[serverName];
|
|
8
|
+
if (!options.recommendedOnly) {
|
|
9
|
+
for (const envName of server.required_env) {
|
|
10
|
+
entries.push({ server: serverName, kind: "required", name: envName, value: "" });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
if (!options.requiredOnly) {
|
|
14
|
+
for (const envName of server.recommended_env) {
|
|
15
|
+
entries.push({ server: serverName, kind: "recommended", name: envName, value: "" });
|
|
16
|
+
}
|
|
17
|
+
for (const [envName, value] of Object.entries(server.env)) {
|
|
18
|
+
entries.push({ server: serverName, kind: "default", name: envName, value });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return dedupeMcpEnvironmentEntries(entries);
|
|
23
|
+
}
|
|
24
|
+
export function formatMcpDotenv(servers, options = {}) {
|
|
25
|
+
const entries = listMcpEnvironmentEntries(servers, options);
|
|
26
|
+
const lines = [
|
|
27
|
+
"# Academic research MCP environment example.",
|
|
28
|
+
"# Copy to .env.local, your shell profile, or your MCP client secret store.",
|
|
29
|
+
"# Do not commit filled secrets. Empty values mean optional or user-supplied.",
|
|
30
|
+
""
|
|
31
|
+
];
|
|
32
|
+
let previousServer = "";
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (entry.server !== previousServer) {
|
|
35
|
+
if (previousServer)
|
|
36
|
+
lines.push("");
|
|
37
|
+
lines.push(`# ${entry.server} environment`);
|
|
38
|
+
previousServer = entry.server;
|
|
39
|
+
}
|
|
40
|
+
lines.push(`${entry.name}=${dotenvValue(entry.value)}`);
|
|
41
|
+
}
|
|
42
|
+
if (entries.length === 0) {
|
|
43
|
+
lines.push("# No environment variables are required for the selected MCP servers.");
|
|
44
|
+
}
|
|
45
|
+
return `${lines.join("\n")}\n`;
|
|
46
|
+
}
|
|
47
|
+
export async function readMcpEnvironmentFile(path) {
|
|
48
|
+
return parseDotenv(await readFile(path, "utf8"), path);
|
|
49
|
+
}
|
|
50
|
+
export function mergeMcpEnvironment(baseEnv = process.env, fileEnv = {}) {
|
|
51
|
+
const merged = { ...baseEnv };
|
|
52
|
+
for (const [name, value] of Object.entries(fileEnv)) {
|
|
53
|
+
if (value || !(name in merged))
|
|
54
|
+
merged[name] = value;
|
|
55
|
+
}
|
|
56
|
+
return merged;
|
|
57
|
+
}
|
|
58
|
+
function assertKnownMcpServers(servers) {
|
|
59
|
+
const unknown = servers.filter((server) => !AGENT_STACK.mcp_servers[server]);
|
|
60
|
+
if (unknown.length > 0) {
|
|
61
|
+
throw new Error(`unknown MCP server: ${unknown.join(", ")}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function dedupeMcpEnvironmentEntries(entries) {
|
|
65
|
+
const priority = { required: 0, default: 1, recommended: 2 };
|
|
66
|
+
const byName = new Map();
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
const previous = byName.get(entry.name);
|
|
69
|
+
if (!previous || priority[entry.kind] < priority[previous.kind]) {
|
|
70
|
+
byName.set(entry.name, entry);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return [...byName.values()];
|
|
74
|
+
}
|
|
75
|
+
function dotenvValue(value) {
|
|
76
|
+
if (!value)
|
|
77
|
+
return "";
|
|
78
|
+
if (/^[A-Za-z0-9_./:-]+$/.test(value))
|
|
79
|
+
return value;
|
|
80
|
+
return JSON.stringify(value);
|
|
81
|
+
}
|
|
82
|
+
function parseDotenv(raw, path) {
|
|
83
|
+
const env = {};
|
|
84
|
+
const lines = raw.split(/\r?\n/);
|
|
85
|
+
for (const [index, line] of lines.entries()) {
|
|
86
|
+
let text = line.trim();
|
|
87
|
+
if (!text || text.startsWith("#"))
|
|
88
|
+
continue;
|
|
89
|
+
if (text.startsWith("export "))
|
|
90
|
+
text = text.slice("export ".length).trimStart();
|
|
91
|
+
const equals = text.indexOf("=");
|
|
92
|
+
if (equals === -1) {
|
|
93
|
+
throw new Error(`${path}:${index + 1}: expected KEY=value`);
|
|
94
|
+
}
|
|
95
|
+
const key = text.slice(0, equals).trim();
|
|
96
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
97
|
+
throw new Error(`${path}:${index + 1}: invalid environment variable name: ${key}`);
|
|
98
|
+
}
|
|
99
|
+
env[key] = parseDotenvValue(text.slice(equals + 1).trim(), path, index + 1);
|
|
100
|
+
}
|
|
101
|
+
return env;
|
|
102
|
+
}
|
|
103
|
+
function parseDotenvValue(value, path, line) {
|
|
104
|
+
if (!value)
|
|
105
|
+
return "";
|
|
106
|
+
const quote = value[0];
|
|
107
|
+
if (quote === "'" || quote === '"') {
|
|
108
|
+
if (!value.endsWith(quote) || value.length === 1) {
|
|
109
|
+
throw new Error(`${path}:${line}: unterminated quoted value`);
|
|
110
|
+
}
|
|
111
|
+
const unquoted = value.slice(1, -1);
|
|
112
|
+
if (quote === "'")
|
|
113
|
+
return unquoted;
|
|
114
|
+
return unquoted
|
|
115
|
+
.replaceAll("\\n", "\n")
|
|
116
|
+
.replaceAll("\\r", "\r")
|
|
117
|
+
.replaceAll("\\t", "\t")
|
|
118
|
+
.replaceAll('\\"', '"')
|
|
119
|
+
.replaceAll("\\\\", "\\");
|
|
120
|
+
}
|
|
121
|
+
return value.replace(/\s+#.*$/, "").trimEnd();
|
|
122
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type McpProbeStatus = "ok" | "manual" | "missing-env" | "runtime-missing" | "startup-failed" | "protocol-error" | "timeout";
|
|
2
|
+
export interface McpProbeServerResult {
|
|
3
|
+
server: string;
|
|
4
|
+
status: McpProbeStatus;
|
|
5
|
+
detail: string;
|
|
6
|
+
}
|
|
7
|
+
export interface McpProbeResult {
|
|
8
|
+
ok: boolean;
|
|
9
|
+
results: McpProbeServerResult[];
|
|
10
|
+
}
|
|
11
|
+
export declare function probeMcpServerList(root: string, servers: string[], env: NodeJS.ProcessEnv, timeoutMs: number, clientVersion?: string): Promise<McpProbeResult>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
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") {
|
|
6
|
+
assertKnownMcpServers(servers);
|
|
7
|
+
const results = [];
|
|
8
|
+
for (const name of servers) {
|
|
9
|
+
const server = AGENT_STACK.mcp_servers[name];
|
|
10
|
+
const missingRequired = server.required_env.filter((envName) => !envHasValue(env, envName));
|
|
11
|
+
if (missingRequired.length > 0) {
|
|
12
|
+
results.push({ server: name, status: "missing-env", detail: missingRequired.join(",") });
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (!server.command) {
|
|
16
|
+
results.push({ server: name, status: "manual", detail: server.local_service || "manual setup only" });
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (!commandExists(server.command, env)) {
|
|
20
|
+
results.push({ server: name, status: "runtime-missing", detail: server.command });
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
results.push(await probeMcpServerProcess(root, name, server.command, server.args, { ...server.env, ...env }, timeoutMs, clientVersion));
|
|
24
|
+
}
|
|
25
|
+
return { ok: results.every((result) => result.status === "ok"), results };
|
|
26
|
+
}
|
|
27
|
+
function assertKnownMcpServers(servers) {
|
|
28
|
+
const unknown = servers.filter((server) => !AGENT_STACK.mcp_servers[server]);
|
|
29
|
+
if (unknown.length > 0) {
|
|
30
|
+
throw new Error(`unknown MCP server: ${unknown.join(", ")}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function envHasValue(env, name) {
|
|
34
|
+
return typeof env[name] === "string" && env[name] !== "";
|
|
35
|
+
}
|
|
36
|
+
function commandExists(command, env = process.env) {
|
|
37
|
+
if (!command)
|
|
38
|
+
return false;
|
|
39
|
+
if (command.includes("/") || command.includes("\\"))
|
|
40
|
+
return existsSync(command);
|
|
41
|
+
const pathValue = env.PATH ?? "";
|
|
42
|
+
const extensions = process.platform === "win32"
|
|
43
|
+
? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";")
|
|
44
|
+
: [""];
|
|
45
|
+
for (const directory of pathValue.split(delimiter).filter(Boolean)) {
|
|
46
|
+
for (const extension of extensions) {
|
|
47
|
+
const hasExtension = extension && command.toLowerCase().endsWith(extension.toLowerCase());
|
|
48
|
+
const candidate = join(directory, hasExtension ? command : `${command}${extension}`);
|
|
49
|
+
if (existsSync(candidate))
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
async function probeMcpServerProcess(root, server, command, args, env, timeoutMs, clientVersion) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
let settled = false;
|
|
58
|
+
let stderr = "";
|
|
59
|
+
let stdout = Buffer.alloc(0);
|
|
60
|
+
const child = spawn(command, args, {
|
|
61
|
+
cwd: root,
|
|
62
|
+
env,
|
|
63
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
64
|
+
});
|
|
65
|
+
const timer = setTimeout(() => finish("timeout", `${timeoutMs}ms`), timeoutMs);
|
|
66
|
+
function finish(status, detail) {
|
|
67
|
+
if (settled)
|
|
68
|
+
return;
|
|
69
|
+
settled = true;
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
child.kill();
|
|
72
|
+
resolve({ server, status, detail: detail || "-" });
|
|
73
|
+
}
|
|
74
|
+
child.on("error", (error) => {
|
|
75
|
+
finish(error.code === "ENOENT" ? "runtime-missing" : "startup-failed", error.message);
|
|
76
|
+
});
|
|
77
|
+
child.on("close", (code) => {
|
|
78
|
+
if (!settled)
|
|
79
|
+
finish("startup-failed", stderr.trim() || `process exited with code ${code ?? "unknown"}`);
|
|
80
|
+
});
|
|
81
|
+
child.stdin.on("error", (error) => {
|
|
82
|
+
finish("startup-failed", error.message);
|
|
83
|
+
});
|
|
84
|
+
child.stderr.on("data", (chunk) => {
|
|
85
|
+
stderr += chunk.toString("utf8");
|
|
86
|
+
});
|
|
87
|
+
child.stdout.on("data", (chunk) => {
|
|
88
|
+
stdout = Buffer.concat([stdout, chunk]);
|
|
89
|
+
try {
|
|
90
|
+
for (const message of drainMcpMessages()) {
|
|
91
|
+
handleMcpProbeMessage(message);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
finish("protocol-error", error instanceof Error ? error.message : String(error));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
try {
|
|
99
|
+
child.stdin.write(encodeMcpMessage({
|
|
100
|
+
jsonrpc: "2.0",
|
|
101
|
+
id: 1,
|
|
102
|
+
method: "initialize",
|
|
103
|
+
params: {
|
|
104
|
+
protocolVersion: "2025-06-18",
|
|
105
|
+
capabilities: {},
|
|
106
|
+
clientInfo: { name: "academic-research-cli", version: clientVersion }
|
|
107
|
+
}
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
finish("startup-failed", error instanceof Error ? error.message : String(error));
|
|
112
|
+
}
|
|
113
|
+
function handleMcpProbeMessage(message) {
|
|
114
|
+
if (message.id === 1) {
|
|
115
|
+
if (message.error) {
|
|
116
|
+
finish("protocol-error", formatJsonRpcError(message.error));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
child.stdin.write(encodeMcpMessage({
|
|
120
|
+
jsonrpc: "2.0",
|
|
121
|
+
method: "notifications/initialized",
|
|
122
|
+
params: {}
|
|
123
|
+
}));
|
|
124
|
+
child.stdin.write(encodeMcpMessage({
|
|
125
|
+
jsonrpc: "2.0",
|
|
126
|
+
id: 2,
|
|
127
|
+
method: "tools/list",
|
|
128
|
+
params: {}
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
else if (message.id === 2) {
|
|
132
|
+
if (message.error) {
|
|
133
|
+
finish("protocol-error", formatJsonRpcError(message.error));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const result = typeof message.result === "object" && message.result !== null
|
|
137
|
+
? message.result
|
|
138
|
+
: {};
|
|
139
|
+
const toolCount = Array.isArray(result.tools) ? result.tools.length : "unknown";
|
|
140
|
+
finish("ok", `tools=${toolCount}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function drainMcpMessages() {
|
|
144
|
+
const messages = [];
|
|
145
|
+
while (true) {
|
|
146
|
+
const separator = stdout.indexOf("\r\n\r\n");
|
|
147
|
+
if (separator === -1)
|
|
148
|
+
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)
|
|
157
|
+
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);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function encodeMcpMessage(message) {
|
|
169
|
+
const body = JSON.stringify(message);
|
|
170
|
+
return `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`;
|
|
171
|
+
}
|
|
172
|
+
function formatJsonRpcError(error) {
|
|
173
|
+
if (typeof error === "object" && error !== null && "message" in error) {
|
|
174
|
+
return String(error.message);
|
|
175
|
+
}
|
|
176
|
+
return JSON.stringify(error);
|
|
177
|
+
}
|
package/dist/src/project.js
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import YAML from "yaml";
|
|
5
|
-
import { DEFAULT_AGENT, initializeCapabilities, installSkills } from "./capabilities.js";
|
|
5
|
+
import { DEFAULT_AGENT, initializeCapabilities, installSkills, writeMcpEnvironmentExample } from "./capabilities.js";
|
|
6
6
|
import { assertKnownAgentTarget } from "./agents.js";
|
|
7
7
|
import { copyDirectory, exists, isNonEmptyDirectory, movePath, readJson, writeJson } from "./files.js";
|
|
8
8
|
import { packageify, slugify, titleFromSlug } from "./names.js";
|
|
@@ -100,6 +100,7 @@ export async function createProject(options) {
|
|
|
100
100
|
await personalizeProject(target, { title, slug, packageName, profile: options.profile ?? "academic-general" });
|
|
101
101
|
await writeGeneratedPackageJson(target, { slug });
|
|
102
102
|
await writeAgentStack(target);
|
|
103
|
+
await writeMcpEnvironmentExample(target);
|
|
103
104
|
await initializeCapabilities(target, { preset, agent });
|
|
104
105
|
if (options.installSkills) {
|
|
105
106
|
await installSkills(target, preset);
|
|
@@ -121,7 +122,7 @@ export async function renameProject(root, options) {
|
|
|
121
122
|
profile: config.project.profile,
|
|
122
123
|
previousPackage
|
|
123
124
|
});
|
|
124
|
-
await writeGeneratedPackageJson(target, { slug });
|
|
125
|
+
await writeGeneratedPackageJson(target, { slug, preserveExistingSpec: true });
|
|
125
126
|
return { root: target, title, slug, packageName };
|
|
126
127
|
}
|
|
127
128
|
export async function doctorProject(root) {
|
|
@@ -129,6 +130,7 @@ export async function doctorProject(root) {
|
|
|
129
130
|
const errors = [];
|
|
130
131
|
const required = [
|
|
131
132
|
"README.md",
|
|
133
|
+
".env.example",
|
|
132
134
|
"package.json",
|
|
133
135
|
"pyproject.toml",
|
|
134
136
|
"AGENTS.md",
|
|
@@ -137,6 +139,7 @@ export async function doctorProject(root) {
|
|
|
137
139
|
"configs/capabilities.yaml",
|
|
138
140
|
"docs/agent/capability-profile.md",
|
|
139
141
|
"docs/agent/mcp-setup.md",
|
|
142
|
+
"docs/agent/mcp-client-setup.md",
|
|
140
143
|
"docs/agent/generated",
|
|
141
144
|
"scripts/README.md",
|
|
142
145
|
"sources/source-ledger.csv",
|
|
@@ -210,11 +213,13 @@ async function personalizeProject(root, { title, slug, packageName, profile, pre
|
|
|
210
213
|
}
|
|
211
214
|
}
|
|
212
215
|
}
|
|
213
|
-
async function writeGeneratedPackageJson(root, { slug }) {
|
|
216
|
+
async function writeGeneratedPackageJson(root, { slug, preserveExistingSpec = false }) {
|
|
214
217
|
const path = join(root, "package.json");
|
|
215
218
|
const data = await readJson(path);
|
|
216
219
|
const existingSpec = data.devDependencies?.["create-academic-research"];
|
|
217
|
-
const packageSpec = process.env.CREATE_ACADEMIC_RESEARCH_PACKAGE_SPEC ??
|
|
220
|
+
const packageSpec = process.env.CREATE_ACADEMIC_RESEARCH_PACKAGE_SPEC ??
|
|
221
|
+
(preserveExistingSpec ? existingSpec : undefined) ??
|
|
222
|
+
await currentPackageVersion();
|
|
218
223
|
data.name = slug;
|
|
219
224
|
data.devDependencies = {
|
|
220
225
|
...(data.devDependencies ?? {}),
|
|
@@ -222,6 +227,13 @@ async function writeGeneratedPackageJson(root, { slug }) {
|
|
|
222
227
|
};
|
|
223
228
|
await writeJson(path, data);
|
|
224
229
|
}
|
|
230
|
+
async function currentPackageVersion() {
|
|
231
|
+
const packageJson = await readJson(join(packageRoot, "package.json"));
|
|
232
|
+
if (!packageJson.version) {
|
|
233
|
+
throw new Error("package.json missing version");
|
|
234
|
+
}
|
|
235
|
+
return packageJson.version;
|
|
236
|
+
}
|
|
225
237
|
async function writeAgentStack(root) {
|
|
226
238
|
await writeFile(join(root, "configs/agent-stack.yaml"), YAML.stringify(AGENT_STACK), "utf8");
|
|
227
239
|
}
|
package/dist/src/stack.js
CHANGED
|
@@ -180,7 +180,7 @@ export const AGENT_STACK = {
|
|
|
180
180
|
recommended_env: [],
|
|
181
181
|
local_service: "",
|
|
182
182
|
smoke_test: "Search works by title or DOI and confirm stable OpenAlex IDs.",
|
|
183
|
-
risks: "The selected local server requires OPENALEX_API_KEY. OpenAlex keys are free
|
|
183
|
+
risks: "The selected local server requires OPENALEX_API_KEY. OpenAlex keys are free and include a free daily quota; check current credit limits, smoke-test coverage, and inspect cost headers before high-volume work."
|
|
184
184
|
},
|
|
185
185
|
crossref: {
|
|
186
186
|
readiness: "manual",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-academic-research",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Create and manage agent-ready academic research repositories.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"dist",
|
|
37
37
|
"template",
|
|
38
38
|
"README.md",
|
|
39
|
+
"CHANGELOG.md",
|
|
39
40
|
"LICENSE",
|
|
40
41
|
"SECURITY.md"
|
|
41
42
|
],
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Academic research MCP environment example.
|
|
2
|
+
# Copy to .env.local, your shell profile, or your MCP client secret store.
|
|
3
|
+
# Do not commit filled secrets. Empty values mean optional or user-supplied.
|
|
4
|
+
|
|
5
|
+
# semantic-scholar environment
|
|
6
|
+
SEMANTIC_SCHOLAR_API_KEY=
|
|
7
|
+
|
|
8
|
+
# openalex environment
|
|
9
|
+
OPENALEX_API_KEY=
|
|
10
|
+
|
|
11
|
+
# pubmed environment
|
|
12
|
+
NCBI_API_KEY=
|
|
13
|
+
NCBI_ADMIN_EMAIL=
|
|
14
|
+
MCP_TRANSPORT_TYPE=stdio
|
|
15
|
+
MCP_LOG_LEVEL=warning
|
|
16
|
+
|
|
17
|
+
# overleaf environment
|
|
18
|
+
OVERLEAF_TOKEN=
|
|
19
|
+
PROJECT_ID=
|
|
20
|
+
|
|
21
|
+
# paper-search environment
|
|
22
|
+
PAPER_SEARCH_MCP_UNPAYWALL_EMAIL=
|
|
23
|
+
PAPER_SEARCH_MCP_SEMANTIC_SCHOLAR_API_KEY=
|
package/template/AGENTS.md
CHANGED
|
@@ -11,6 +11,8 @@ scholarly record.
|
|
|
11
11
|
- Tie claims to sources, datasets, experiment records, or decision records.
|
|
12
12
|
- Update durable records when project knowledge changes.
|
|
13
13
|
- Keep large data, generated caches, credentials, and private review material out of git.
|
|
14
|
+
- Keep `.env.example` as a public reference only; never commit filled `.env`,
|
|
15
|
+
`.env.local`, API keys, tokens, cookies, or browser sessions.
|
|
14
16
|
|
|
15
17
|
## First Read
|
|
16
18
|
|
package/template/README.md
CHANGED
|
@@ -61,13 +61,15 @@ npx academic-research skills install source-ingestion sota-literature-review
|
|
|
61
61
|
npx academic-research skills list
|
|
62
62
|
npx academic-research skills status
|
|
63
63
|
npx academic-research setup
|
|
64
|
+
npx academic-research mcp env --write .env.example --all
|
|
64
65
|
npx academic-research mcp list
|
|
65
66
|
npx academic-research mcp env openalex semantic-scholar zotero
|
|
66
67
|
npx academic-research mcp enable arxiv dblp
|
|
67
68
|
npx academic-research mcp commands arxiv
|
|
68
69
|
npx academic-research mcp install arxiv
|
|
69
|
-
npx academic-research mcp smoke
|
|
70
|
-
npx academic-research mcp doctor
|
|
70
|
+
npx academic-research mcp smoke --env-file .env.local
|
|
71
|
+
npx academic-research mcp doctor --env-file .env.local
|
|
72
|
+
npx academic-research mcp probe arxiv --timeout-ms 5000
|
|
71
73
|
```
|
|
72
74
|
|
|
73
75
|
`skills list` reports installed project-local skills. `skills presets` reports
|
|
@@ -78,11 +80,18 @@ enable optional servers. `mcp install` runs only finite tool installation
|
|
|
78
80
|
commands; runtime-only `uvx`/`npx` MCP servers may have no install step and are
|
|
79
81
|
started later by the MCP client.
|
|
80
82
|
|
|
83
|
+
`.env.example` is the committed MCP environment reference. Regenerate it with
|
|
84
|
+
`mcp env --write .env.example --all`. Copy it to `.env.local`, your shell
|
|
85
|
+
profile, or your MCP client secret store when secrets are needed. Filled `.env`
|
|
86
|
+
files are ignored by git. `mcp doctor` checks the current process environment
|
|
87
|
+
unless you explicitly pass `--env-file .env.local`.
|
|
88
|
+
|
|
81
89
|
`setup` prints the current project capability state, installed skill counts,
|
|
82
90
|
enabled MCP records, and the next onboarding commands without changing files.
|
|
83
91
|
`mcp smoke` performs a non-launching MCP readiness check: it reports required
|
|
84
92
|
env vars, local/manual setup, and whether client runtime commands such as `uvx`
|
|
85
|
-
or `npx` are available.
|
|
93
|
+
or `npx` are available. `mcp probe` is opt-in and starts selected MCP servers
|
|
94
|
+
for a real stdio JSON-RPC handshake.
|
|
86
95
|
|
|
87
96
|
`default` installs the companion academic research skill package and keeps the
|
|
88
97
|
MCP records focused on low-friction arXiv discovery. `literature` and `full`
|
|
@@ -90,3 +99,5 @@ add DBLP for computer science bibliography. Credentialed, local-service, or
|
|
|
90
99
|
domain-specific MCP servers such as OpenAlex, Semantic Scholar, PubMed, Zotero,
|
|
91
100
|
and Overleaf should be enabled only after reading `docs/agent/mcp-setup.md` and
|
|
92
101
|
checking their prerequisites with `mcp env`.
|
|
102
|
+
|
|
103
|
+
See `docs/getting-started.md` for the recommended first session workflow.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# MCP Client Setup
|
|
2
|
+
|
|
3
|
+
Generated MCP snippets live in `docs/agent/generated/`. They are client-ready
|
|
4
|
+
configuration fragments, not live tools by themselves. The active MCP client
|
|
5
|
+
must load the generated snippet and must receive any required environment
|
|
6
|
+
variables from the shell, a local untracked env file, or the client's secret
|
|
7
|
+
store.
|
|
8
|
+
|
|
9
|
+
## Files
|
|
10
|
+
|
|
11
|
+
- `docs/agent/generated/mcp.json`: generic/default generated snippet.
|
|
12
|
+
- `docs/agent/generated/<agent>-mcp.json`: generated when a specific agent is
|
|
13
|
+
selected.
|
|
14
|
+
- `.env.example`: committed reference for MCP environment variables.
|
|
15
|
+
- `.env.local`: recommended local untracked file for filled secrets.
|
|
16
|
+
|
|
17
|
+
## Environment
|
|
18
|
+
|
|
19
|
+
Regenerate the committed reference from the current MCP catalog with:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx academic-research mcp env --write .env.example --all
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Create a private local file when needed:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cp .env.example .env.local
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Do not commit filled `.env`, `.env.local`, tokens, cookies, or browser sessions.
|
|
32
|
+
`mcp doctor`, `mcp smoke`, and `mcp probe` check the current process
|
|
33
|
+
environment unless you explicitly pass `--env-file .env.local`.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx academic-research mcp doctor --env-file .env.local
|
|
37
|
+
npx academic-research mcp smoke --env-file .env.local
|
|
38
|
+
npx academic-research mcp probe arxiv --timeout-ms 5000
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Client Notes
|
|
42
|
+
|
|
43
|
+
For Codex, Claude Code, Cursor, or another MCP client, load the generated
|
|
44
|
+
snippet that matches the active agent target:
|
|
45
|
+
|
|
46
|
+
- `docs/agent/generated/mcp.json` for the universal/default target.
|
|
47
|
+
- `docs/agent/generated/codex-mcp.json` when the project was created with
|
|
48
|
+
`--agent codex`.
|
|
49
|
+
- `docs/agent/generated/claude-code-mcp.json` when using Claude Code.
|
|
50
|
+
- `docs/agent/generated/cursor-mcp.json` when using Cursor.
|
|
51
|
+
|
|
52
|
+
The generated snippet is project documentation until the client loads it. If a
|
|
53
|
+
client has its own secret store, prefer that store for API keys and tokens. If
|
|
54
|
+
the client inherits shell environment variables, start it from a shell where the
|
|
55
|
+
required variables are already exported.
|
|
56
|
+
|
|
57
|
+
## Workflow
|
|
58
|
+
|
|
59
|
+
1. Enable only the MCP servers needed for the current research task.
|
|
60
|
+
2. Inspect prerequisites with `npx academic-research mcp env <server>`.
|
|
61
|
+
3. Put required secrets in the MCP client secret store, shell, or `.env.local`.
|
|
62
|
+
4. Run `npx academic-research mcp smoke --env-file .env.local`.
|
|
63
|
+
5. Run `npx academic-research mcp probe <server>` only when you want to start
|
|
64
|
+
the server and verify a real stdio handshake.
|
|
65
|
+
6. Load the generated snippet in the MCP client.
|
|
66
|
+
7. Treat MCP output as retrieval metadata until it is ingested into repository
|
|
67
|
+
source records.
|
|
@@ -2,3 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Record active MCP servers, install commands, auth requirements, smoke tests,
|
|
4
4
|
and known risks here.
|
|
5
|
+
|
|
6
|
+
Use `.env.example` as the committed environment reference. Put filled values in
|
|
7
|
+
`.env.local`, the shell, or the MCP client secret store. Regenerate the example
|
|
8
|
+
with `npx academic-research mcp env --write .env.example --all`.
|
|
9
|
+
|
|
10
|
+
Use `npx academic-research mcp doctor --env-file .env.local` when you want the
|
|
11
|
+
CLI to read an explicit local env file. Use `npx academic-research mcp probe
|
|
12
|
+
<server>` only when you want to start a selected MCP server and verify a real
|
|
13
|
+
stdio handshake.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
Use this path for the first working session in a new research repository.
|
|
4
|
+
|
|
5
|
+
## 1. Check The Repository
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npx academic-research doctor
|
|
10
|
+
npx academic-research setup
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`doctor` checks required files and structural contracts. `setup` prints the
|
|
14
|
+
active skill preset, installed skill count, enabled MCP records, and next
|
|
15
|
+
commands without changing files.
|
|
16
|
+
|
|
17
|
+
## 2. Install Project-Local Skills
|
|
18
|
+
|
|
19
|
+
Install the default academic research skill package:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx academic-research skills install --preset default
|
|
23
|
+
npx academic-research skills status
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Use `enhanced` only when the project also needs complementary development,
|
|
27
|
+
document, frontend, testing, and conversion skills:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx academic-research skills install --preset enhanced
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 3. Prepare MCP Environment
|
|
34
|
+
|
|
35
|
+
Keep `.env.example` committed and empty of real secrets. Put filled values in
|
|
36
|
+
`.env.local`, your shell, or your MCP client secret store.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx academic-research mcp env --write .env.example --all
|
|
40
|
+
cp .env.example .env.local
|
|
41
|
+
npx academic-research mcp env openalex semantic-scholar zotero
|
|
42
|
+
npx academic-research mcp doctor --env-file .env.local
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`mcp smoke` is a non-launching readiness check. `mcp probe` is opt-in and starts
|
|
46
|
+
MCP processes for a real stdio handshake.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx academic-research mcp smoke --env-file .env.local
|
|
50
|
+
npx academic-research mcp probe arxiv --timeout-ms 5000
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 4. Start Source Work
|
|
54
|
+
|
|
55
|
+
Put source originals and metadata in the source layer before synthesis.
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
sources/pdfs/ native PDFs
|
|
59
|
+
sources/markdown/ derived Markdown
|
|
60
|
+
sources/metadata/ downloaded metadata or query exports
|
|
61
|
+
sources/bib/ BibTeX and citation audits
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Update `sources/source-ledger.csv` whenever a paper, report, dataset, or web
|
|
65
|
+
source becomes evidence for the project.
|
|
66
|
+
|
|
67
|
+
## 5. Build The First SOTA Pass
|
|
68
|
+
|
|
69
|
+
Use `sota/search-strategy.md` to record search terms, databases, dates, and
|
|
70
|
+
inclusion criteria. Put screened sources in `sota/literature-matrix.csv`, then
|
|
71
|
+
summarize stable conclusions in `sota/synthesis.md`.
|
|
72
|
+
|
|
73
|
+
Do not treat MCP output as final evidence until the relevant source has been
|
|
74
|
+
ingested, deduplicated, and tied to a source record.
|
|
75
|
+
|
|
76
|
+
## 6. Keep Durable Memory Current
|
|
77
|
+
|
|
78
|
+
Update the wiki when project knowledge changes:
|
|
79
|
+
|
|
80
|
+
- `wiki/log.md`: chronological actions and decisions.
|
|
81
|
+
- `wiki/index.md`: navigation index.
|
|
82
|
+
- `wiki/synthesis.md`: current project-level interpretation.
|
|
83
|
+
- `wiki/open_questions.md`: unresolved questions.
|
|
84
|
+
- `wiki/contradictions.md`: conflicting sources, claims, or runs.
|
|
85
|
+
|
|
86
|
+
Prefer small, source-linked updates over long ungrounded summaries.
|
package/template/package.json
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
"mcp:list": "academic-research mcp list",
|
|
12
12
|
"mcp:commands": "academic-research mcp commands",
|
|
13
13
|
"mcp:env": "academic-research mcp env",
|
|
14
|
+
"mcp:dotenv": "academic-research mcp env --write .env.example --all",
|
|
14
15
|
"mcp:smoke": "academic-research mcp smoke",
|
|
15
|
-
"mcp:doctor": "academic-research mcp doctor"
|
|
16
|
+
"mcp:doctor": "academic-research mcp doctor",
|
|
17
|
+
"mcp:probe": "academic-research mcp probe"
|
|
16
18
|
},
|
|
17
19
|
"devDependencies": {
|
|
18
|
-
"create-academic-research": "0.1.
|
|
20
|
+
"create-academic-research": "0.1.10"
|
|
19
21
|
}
|
|
20
22
|
}
|