create-academic-research 0.1.16 → 0.1.17
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 +7 -4
- package/dist/src/capabilities.d.ts +3 -0
- package/dist/src/capabilities.js +106 -21
- package/dist/src/cli.js +53 -4
- package/dist/src/project.js +23 -2
- package/package.json +1 -1
- package/template/README.md +4 -1
- package/template/docs/agent/mcp-client-setup.md +3 -0
- package/template/docs/agent/mcp-setup.md +11 -0
- package/template/docs/getting-started.md +6 -2
package/README.md
CHANGED
|
@@ -115,7 +115,8 @@ Inside a generated project:
|
|
|
115
115
|
```bash
|
|
116
116
|
npm run doctor
|
|
117
117
|
npm run update
|
|
118
|
-
npm run
|
|
118
|
+
npm run update -- --apply
|
|
119
|
+
npm run setup -- --env-file .env.local
|
|
119
120
|
npm run rename -- --title "New Title" --slug new-title --package new_title
|
|
120
121
|
npm run agents:list
|
|
121
122
|
npm run skills:presets
|
|
@@ -177,9 +178,11 @@ edited locally, update reports `skip` instead of overwriting it.
|
|
|
177
178
|
existing files. It adds the research contract, merges lifecycle package scripts,
|
|
178
179
|
and preserves existing README, `.gitignore`, and custom package scripts.
|
|
179
180
|
|
|
180
|
-
`academic-research setup` is
|
|
181
|
+
`academic-research setup` is the friendly post-update recovery command. It
|
|
181
182
|
prints the active preset, agent, skill counts, selected MCP records, and next
|
|
182
|
-
commands
|
|
183
|
+
commands. When you pass `--env-file .env.local`, it may complete safe
|
|
184
|
+
project-local setup such as the Overleaf wrapper and generated MCP snippet. It
|
|
185
|
+
does not register global MCP clients.
|
|
183
186
|
|
|
184
187
|
Skills are project-local by default.
|
|
185
188
|
|
|
@@ -205,7 +208,7 @@ MCP commands are split by side-effect:
|
|
|
205
208
|
| `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`. |
|
|
206
209
|
| `mcp enable` | Select an MCP server in project records and generated snippets. Use `--mode local`, `--mode remote`, or `--mode remote-custom --url <url>` where supported. |
|
|
207
210
|
| `mcp disable` | Remove an MCP server from project records and generated snippets. |
|
|
208
|
-
| `mcp setup` | Run or dry-run finite setup for manual-local integrations such as Overleaf. |
|
|
211
|
+
| `mcp setup` | Run or dry-run finite project-local setup for manual-local integrations such as Overleaf, including wrapper and generated snippet refresh. |
|
|
209
212
|
| `mcp client add` / `mcp client remove` | Register or remove supported MCP client entries, currently Codex, without writing secrets into client config. |
|
|
210
213
|
| `mcp install` | Run finite external tool install commands for selected MCP servers. It must not launch stdio MCP servers. |
|
|
211
214
|
| `mcp uninstall` | Run the external uninstall command when one exists. |
|
|
@@ -166,6 +166,7 @@ interface SkillInstallOptions {
|
|
|
166
166
|
}
|
|
167
167
|
export declare function readCapabilities(root: string): Promise<CapabilityState>;
|
|
168
168
|
export declare function writeCapabilities(root: string, state: Partial<CapabilityState>): Promise<void>;
|
|
169
|
+
export declare function writeCapabilityGeneratedFiles(root: string, state: CapabilityState): Promise<void>;
|
|
169
170
|
export declare function writeMcpEnvironmentExample(root: string): Promise<void>;
|
|
170
171
|
export declare function initializeCapabilities(root: string, options?: InitializeCapabilitiesOptions): Promise<void>;
|
|
171
172
|
export declare function buildSkillInstallCommands(root: string, preset?: string, options?: SkillInstallOptions): Promise<string[][]>;
|
|
@@ -198,4 +199,6 @@ export declare function setupMcpServer(root: string, serverName: string, options
|
|
|
198
199
|
export declare function clientAddMcpServer(root: string, serverName: string, options?: McpClientOptions, runner?: Runner): Promise<McpClientResult>;
|
|
199
200
|
export declare function clientRemoveMcpServer(root: string, serverName: string, options?: McpClientOptions, runner?: Runner): Promise<McpClientResult>;
|
|
200
201
|
export declare function resolveMcpServerForState(state: CapabilityState, serverName: string, mode?: string): ResolvedMcpServer;
|
|
202
|
+
export declare function mcpMissingGeneratedSnippetMessage(serverName: string, server: ResolvedMcpServer, env?: NodeJS.ProcessEnv): string;
|
|
203
|
+
export declare function mcpLocalSetupGitignoreWarning(root: string): Promise<string | undefined>;
|
|
201
204
|
export declare function assertKnownMcpServers(servers: string[]): void;
|
package/dist/src/capabilities.js
CHANGED
|
@@ -24,21 +24,16 @@ export async function readCapabilities(root) {
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
export async function writeCapabilities(root, state) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
preset: state.preset ?? "default",
|
|
31
|
-
scope: "project-local",
|
|
32
|
-
mcp_servers: mcpServers,
|
|
33
|
-
mcp_server_modes: normalizeMcpServerModeMap(state.mcp_server_modes ?? {}, mcpServers),
|
|
34
|
-
mcp_server_remote: normalizeMcpServerRemoteMap(state.mcp_server_remote ?? {}, mcpServers, state.mcp_server_modes ?? {})
|
|
35
|
-
};
|
|
36
|
-
await writeFile(join(root, "configs/capabilities.yaml"), YAML.stringify(serializeCapabilityState(next)), "utf8");
|
|
37
|
-
await writeCapabilityProfile(root, next);
|
|
38
|
-
await writeMcpSetup(root, next);
|
|
39
|
-
await writeMcpSnippet(root, next);
|
|
27
|
+
const next = normalizeCapabilityWriteState(state);
|
|
28
|
+
await writeCapabilityConfig(root, next);
|
|
29
|
+
await writeCapabilityGeneratedFiles(root, next);
|
|
40
30
|
await appendCapabilityLog(root, next);
|
|
41
31
|
}
|
|
32
|
+
export async function writeCapabilityGeneratedFiles(root, state) {
|
|
33
|
+
await writeCapabilityProfile(root, state);
|
|
34
|
+
await writeMcpSetup(root, state);
|
|
35
|
+
await writeMcpSnippet(root, state);
|
|
36
|
+
}
|
|
42
37
|
export async function writeMcpEnvironmentExample(root) {
|
|
43
38
|
await writeFile(join(root, ".env.example"), formatMcpDotenv(Object.keys(AGENT_STACK.mcp_servers)), "utf8");
|
|
44
39
|
}
|
|
@@ -321,6 +316,18 @@ export async function doctorMcpServers(root, options = {}) {
|
|
|
321
316
|
errors.push(`invalid generated MCP snippet: ${snippetPath}: ${message}`);
|
|
322
317
|
}
|
|
323
318
|
}
|
|
319
|
+
const lock = await readCapabilityLock(root);
|
|
320
|
+
const hasManualLocal = enabled.some((name) => {
|
|
321
|
+
if (!AGENT_STACK.mcp_servers[name])
|
|
322
|
+
return false;
|
|
323
|
+
const server = resolveMcpServerForState(state, name, modes[name]);
|
|
324
|
+
return server.connection_mode === "manual-local";
|
|
325
|
+
});
|
|
326
|
+
if (hasManualLocal) {
|
|
327
|
+
const gitignoreWarning = await mcpLocalSetupGitignoreWarning(root);
|
|
328
|
+
if (gitignoreWarning)
|
|
329
|
+
warnings.push(gitignoreWarning);
|
|
330
|
+
}
|
|
324
331
|
for (const name of enabled) {
|
|
325
332
|
const server = resolveMcpServerForState(state, name, modes[name]);
|
|
326
333
|
if (!server)
|
|
@@ -339,7 +346,6 @@ export async function doctorMcpServers(root, options = {}) {
|
|
|
339
346
|
warnings.push(`${name}: requires local service: ${server.local_service}`);
|
|
340
347
|
}
|
|
341
348
|
if (server.connection_mode === "manual-local") {
|
|
342
|
-
const lock = await readCapabilityLock(root);
|
|
343
349
|
if (lock.mcp[name]?.setup?.status !== "ready") {
|
|
344
350
|
warnings.push(`${name}: local setup not complete; run npm run mcp:setup -- ${name} --mode local --env-file .env.local`);
|
|
345
351
|
}
|
|
@@ -349,7 +355,14 @@ export async function doctorMcpServers(root, options = {}) {
|
|
|
349
355
|
continue;
|
|
350
356
|
}
|
|
351
357
|
if (!generatedServers.has(name)) {
|
|
352
|
-
errors.push(
|
|
358
|
+
errors.push(mcpMissingGeneratedSnippetMessage(name, server, env));
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (state.agent === "codex" &&
|
|
362
|
+
server.connection_mode === "manual-local" &&
|
|
363
|
+
lock.mcp[name]?.setup?.status === "ready" &&
|
|
364
|
+
lock.mcp[name]?.clients?.codex?.status !== "registered") {
|
|
365
|
+
warnings.push(`${name} is setup locally but not registered in Codex\nNEXT: npm run mcp:client:add -- ${name} --agent codex`);
|
|
353
366
|
}
|
|
354
367
|
}
|
|
355
368
|
return { ok: errors.length === 0, errors, warnings, enabled };
|
|
@@ -662,8 +675,15 @@ export async function readCapabilityLock(root) {
|
|
|
662
675
|
}
|
|
663
676
|
export async function setupMcpServer(root, serverName, options = {}, runner = defaultRunner) {
|
|
664
677
|
assertKnownMcpServers([serverName]);
|
|
665
|
-
const
|
|
666
|
-
const
|
|
678
|
+
const state = await readCapabilities(root);
|
|
679
|
+
const mode = normalizeMcpMode(serverName, options.mode ?? state.mcp_server_modes?.[serverName]);
|
|
680
|
+
const nextState = normalizeCapabilityWriteState({
|
|
681
|
+
...state,
|
|
682
|
+
mcp_servers: dedupe([...(state.mcp_servers ?? []), serverName]),
|
|
683
|
+
mcp_server_modes: { ...(state.mcp_server_modes ?? {}), [serverName]: mode },
|
|
684
|
+
mcp_server_remote: state.mcp_server_remote ?? {}
|
|
685
|
+
});
|
|
686
|
+
const server = resolveMcpServerForState(nextState, serverName, mode);
|
|
667
687
|
if (serverName !== "overleaf") {
|
|
668
688
|
const commands = server.setup_commands.length > 0 ? server.setup_commands : [];
|
|
669
689
|
return {
|
|
@@ -686,6 +706,7 @@ export async function setupMcpServer(root, serverName, options = {}, runner = de
|
|
|
686
706
|
`cd ${paths.relativeServer} && uv sync`,
|
|
687
707
|
`write wrapper ${paths.relativeWrapper}`
|
|
688
708
|
];
|
|
709
|
+
const gitignoreWarning = await mcpLocalSetupGitignoreWarning(root);
|
|
689
710
|
if (missing.length > 0) {
|
|
690
711
|
return {
|
|
691
712
|
ok: false,
|
|
@@ -693,7 +714,7 @@ export async function setupMcpServer(root, serverName, options = {}, runner = de
|
|
|
693
714
|
mode,
|
|
694
715
|
commands,
|
|
695
716
|
created: [],
|
|
696
|
-
warnings: [],
|
|
717
|
+
warnings: gitignoreWarning ? [gitignoreWarning] : [],
|
|
697
718
|
errors: [`overleaf: missing required environment variable(s): ${missing.join(", ")}`],
|
|
698
719
|
next: [`fill ${missing.join(", ")} in ${options.envFile ?? ".env.local"}`]
|
|
699
720
|
};
|
|
@@ -705,7 +726,7 @@ export async function setupMcpServer(root, serverName, options = {}, runner = de
|
|
|
705
726
|
mode,
|
|
706
727
|
commands,
|
|
707
728
|
created: [],
|
|
708
|
-
warnings: [],
|
|
729
|
+
warnings: gitignoreWarning ? [gitignoreWarning] : [],
|
|
709
730
|
errors: [],
|
|
710
731
|
next: [`run npm run mcp:setup -- overleaf --mode local --env-file ${options.envFile ?? ".env.local"}`]
|
|
711
732
|
};
|
|
@@ -727,17 +748,19 @@ export async function setupMcpServer(root, serverName, options = {}, runner = de
|
|
|
727
748
|
status: "ready",
|
|
728
749
|
server_path: paths.relativeServer,
|
|
729
750
|
wrapper_path: paths.relativeWrapper,
|
|
730
|
-
env_file:
|
|
751
|
+
env_file: mcpEnvFileRecord(root, options.envFile),
|
|
731
752
|
updated_at: nowIso()
|
|
732
753
|
};
|
|
733
754
|
});
|
|
755
|
+
await writeCapabilityConfig(root, nextState);
|
|
756
|
+
await writeCapabilityGeneratedFiles(root, nextState);
|
|
734
757
|
return {
|
|
735
758
|
ok: true,
|
|
736
759
|
server: serverName,
|
|
737
760
|
mode,
|
|
738
761
|
commands,
|
|
739
762
|
created: [paths.relativeWrapper, paths.relativeLauncher],
|
|
740
|
-
warnings: [],
|
|
763
|
+
warnings: gitignoreWarning ? [gitignoreWarning] : [],
|
|
741
764
|
errors: [],
|
|
742
765
|
next: [
|
|
743
766
|
"npm run mcp:client:add -- overleaf --agent codex",
|
|
@@ -824,6 +847,20 @@ function serializeCapabilityState(state) {
|
|
|
824
847
|
serialized.mcp_server_remote = state.mcp_server_remote;
|
|
825
848
|
return serialized;
|
|
826
849
|
}
|
|
850
|
+
function normalizeCapabilityWriteState(state) {
|
|
851
|
+
const mcpServers = [...(state.mcp_servers ?? [])];
|
|
852
|
+
return {
|
|
853
|
+
agent: assertKnownAgentTarget(state.agent),
|
|
854
|
+
preset: state.preset ?? "default",
|
|
855
|
+
scope: "project-local",
|
|
856
|
+
mcp_servers: mcpServers,
|
|
857
|
+
mcp_server_modes: normalizeMcpServerModeMap(state.mcp_server_modes ?? {}, mcpServers),
|
|
858
|
+
mcp_server_remote: normalizeMcpServerRemoteMap(state.mcp_server_remote ?? {}, mcpServers, state.mcp_server_modes ?? {})
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
async function writeCapabilityConfig(root, state) {
|
|
862
|
+
await writeFile(join(root, "configs/capabilities.yaml"), YAML.stringify(serializeCapabilityState(state)), "utf8");
|
|
863
|
+
}
|
|
827
864
|
function normalizeMcpServerModeMap(modes, servers) {
|
|
828
865
|
const selected = new Set(servers);
|
|
829
866
|
const result = {};
|
|
@@ -986,6 +1023,19 @@ function mcpModeKeyLabelForAction(mode) {
|
|
|
986
1023
|
return "manual setup";
|
|
987
1024
|
return "local";
|
|
988
1025
|
}
|
|
1026
|
+
export function mcpMissingGeneratedSnippetMessage(serverName, server, env = process.env) {
|
|
1027
|
+
const lines = [`${serverName}: enabled but missing from generated MCP snippet`];
|
|
1028
|
+
if (server.connection_mode === "manual-local") {
|
|
1029
|
+
lines.push(`NEXT: npm run mcp:setup -- ${serverName} --mode local --env-file .env.local`);
|
|
1030
|
+
const missing = server.required_env.filter((name) => !envHasValue(env, name));
|
|
1031
|
+
if (missing.length > 0)
|
|
1032
|
+
lines.push(`Missing env vars: ${missing.join(", ")}`);
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
lines.push("NEXT: npm run update -- --apply");
|
|
1036
|
+
}
|
|
1037
|
+
return lines.join("\n");
|
|
1038
|
+
}
|
|
989
1039
|
function mcpInstallSkip(serverName, server) {
|
|
990
1040
|
if (server.connection_mode === "manual-local") {
|
|
991
1041
|
return {
|
|
@@ -1098,6 +1148,41 @@ function ensureLockMcpEntry(lock, serverName, server) {
|
|
|
1098
1148
|
lock.mcp[serverName] = entry;
|
|
1099
1149
|
return entry;
|
|
1100
1150
|
}
|
|
1151
|
+
export async function mcpLocalSetupGitignoreWarning(root) {
|
|
1152
|
+
if (await mcpLocalSetupPathIsIgnored(root))
|
|
1153
|
+
return undefined;
|
|
1154
|
+
return [
|
|
1155
|
+
".academic-research/mcp/ is not ignored",
|
|
1156
|
+
"NEXT: add .academic-research/mcp/ to .gitignore before committing local MCP server files"
|
|
1157
|
+
].join("\n");
|
|
1158
|
+
}
|
|
1159
|
+
async function mcpLocalSetupPathIsIgnored(root) {
|
|
1160
|
+
try {
|
|
1161
|
+
const gitignore = await readFile(join(root, ".gitignore"), "utf8");
|
|
1162
|
+
return gitignore
|
|
1163
|
+
.split(/\r?\n/)
|
|
1164
|
+
.map((line) => line.trim())
|
|
1165
|
+
.filter((line) => line && !line.startsWith("#"))
|
|
1166
|
+
.some((line) => {
|
|
1167
|
+
const normalized = line.replaceAll("\\", "/").replace(/^\/+/, "");
|
|
1168
|
+
return normalized === ".academic-research/mcp/" || normalized === ".academic-research/mcp";
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
catch (error) {
|
|
1172
|
+
if (isMissingFileError(error))
|
|
1173
|
+
return false;
|
|
1174
|
+
throw error;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
function mcpEnvFileRecord(root, envFile) {
|
|
1178
|
+
if (!envFile)
|
|
1179
|
+
return ".env.local";
|
|
1180
|
+
const resolved = resolve(root, envFile);
|
|
1181
|
+
const relativePath = relative(root, resolved);
|
|
1182
|
+
if (!relativePath.startsWith("..") && !isAbsolute(relativePath))
|
|
1183
|
+
return toPosix(relativePath);
|
|
1184
|
+
return ".env.local";
|
|
1185
|
+
}
|
|
1101
1186
|
function overleafPaths(root) {
|
|
1102
1187
|
const relativeBase = ".academic-research/mcp/overleaf";
|
|
1103
1188
|
const wrapperDir = join(root, relativeBase);
|
package/dist/src/cli.js
CHANGED
|
@@ -19,6 +19,7 @@ const CREATE_FLAGS = flagSchema([
|
|
|
19
19
|
"no-install-mcp-tools"
|
|
20
20
|
], ["title", "slug", "package", "preset", "profile", "agent"]);
|
|
21
21
|
const ROOT_FLAGS = flagSchema(["help"], ["root"]);
|
|
22
|
+
const SETUP_FLAGS = flagSchema(["help"], ["root", "env-file"]);
|
|
22
23
|
const UPDATE_FLAGS = flagSchema(["help", "dry-run", "apply"], ["root"]);
|
|
23
24
|
const INIT_FLAGS = flagSchema(["help", "install-skills"], ["root", "title", "slug", "package", "preset", "profile", "agent"]);
|
|
24
25
|
const RENAME_FLAGS = flagSchema(["help"], ["root", "title", "slug", "package"]);
|
|
@@ -173,6 +174,12 @@ async function updateCommand(argv) {
|
|
|
173
174
|
if (!apply && result.changes.length > 0) {
|
|
174
175
|
console.log("Run `npm run update -- --apply` from a generated project to write these managed changes.");
|
|
175
176
|
}
|
|
177
|
+
if (apply && await projectLocalMcpSetupNeeded(root)) {
|
|
178
|
+
console.log("");
|
|
179
|
+
console.log("Next:");
|
|
180
|
+
console.log("1. Run npm run setup -- --env-file .env.local to complete project-local tool setup.");
|
|
181
|
+
console.log("2. Run npm run doctor to verify the project.");
|
|
182
|
+
}
|
|
176
183
|
return 0;
|
|
177
184
|
}
|
|
178
185
|
async function initCommand(argv) {
|
|
@@ -198,13 +205,15 @@ async function initCommand(argv) {
|
|
|
198
205
|
return 0;
|
|
199
206
|
}
|
|
200
207
|
async function setupCommand(argv) {
|
|
201
|
-
const parsed = parseFlags(argv,
|
|
208
|
+
const parsed = parseFlags(argv, SETUP_FLAGS);
|
|
202
209
|
if (flagBool(parsed.flags, "help")) {
|
|
203
210
|
printSetupHelp();
|
|
204
211
|
return 0;
|
|
205
212
|
}
|
|
206
213
|
assertNoArguments(parsed.positionals, "setup");
|
|
207
214
|
const root = resolve(flagString(parsed.flags, "root") ?? ".");
|
|
215
|
+
const env = await mcpCommandEnvironment(root, parsed.flags);
|
|
216
|
+
const setupResults = await runProjectLocalMcpSetup(root, env, flagString(parsed.flags, "env-file"));
|
|
208
217
|
const project = await doctorProject(root);
|
|
209
218
|
const state = await readCapabilities(root);
|
|
210
219
|
const skills = await listInstalledSkills(root);
|
|
@@ -219,13 +228,30 @@ async function setupCommand(argv) {
|
|
|
219
228
|
console.log(`installed_skill_copies\t${skills.length}`);
|
|
220
229
|
console.log(`mcp_enabled\t${state.mcp_servers.length > 0 ? state.mcp_servers.join(",") : "none"}`);
|
|
221
230
|
console.log(`mcp_selected\t${state.mcp_servers.length > 0 ? state.mcp_servers.join(",") : "none"}`);
|
|
231
|
+
for (const result of setupResults) {
|
|
232
|
+
if (result.ok) {
|
|
233
|
+
console.log(`Completed project-local MCP setup: ${result.server}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
222
236
|
if (!project.ok) {
|
|
223
237
|
for (const error of project.errors)
|
|
224
238
|
console.error(`ERROR: ${error}`);
|
|
225
239
|
}
|
|
226
|
-
|
|
240
|
+
const setupWarnings = [];
|
|
241
|
+
for (const result of setupResults) {
|
|
242
|
+
for (const error of result.errors)
|
|
243
|
+
console.error(`ERROR: ${error}`);
|
|
244
|
+
setupWarnings.push(...result.warnings);
|
|
245
|
+
if (!result.ok && result.next.length > 0) {
|
|
246
|
+
console.log("");
|
|
247
|
+
console.log(`Next for ${result.server}`);
|
|
248
|
+
for (const command of result.next)
|
|
249
|
+
console.log(command);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (const warning of dedupeStrings([...setupWarnings, ...project.warnings]))
|
|
227
253
|
console.warn(`WARN: ${warning}`);
|
|
228
|
-
const lifecycle = await getMcpLifecycleStatus(root);
|
|
254
|
+
const lifecycle = await getMcpLifecycleStatus(root, { env });
|
|
229
255
|
console.log("");
|
|
230
256
|
console.log("Next Commands");
|
|
231
257
|
console.log(`npm run skills:install -- --preset ${state.preset}`);
|
|
@@ -649,6 +675,28 @@ function setupNextCommands(item) {
|
|
|
649
675
|
commands.push(item.next.replace(/^run /, ""));
|
|
650
676
|
return dedupeStrings(commands);
|
|
651
677
|
}
|
|
678
|
+
async function runProjectLocalMcpSetup(root, env, envFile) {
|
|
679
|
+
const lifecycle = await getMcpLifecycleStatus(root, { env });
|
|
680
|
+
const results = [];
|
|
681
|
+
for (const item of lifecycle.servers) {
|
|
682
|
+
if (!item.selected || item.connection_mode !== "manual-local")
|
|
683
|
+
continue;
|
|
684
|
+
if (item.install === "ready" && item.snippet !== "missing")
|
|
685
|
+
continue;
|
|
686
|
+
results.push(await setupMcpServer(root, item.id, {
|
|
687
|
+
mode: item.mode_key,
|
|
688
|
+
envFile: envFile ?? ".env.local",
|
|
689
|
+
env
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
return results;
|
|
693
|
+
}
|
|
694
|
+
async function projectLocalMcpSetupNeeded(root) {
|
|
695
|
+
const lifecycle = await getMcpLifecycleStatus(root);
|
|
696
|
+
return lifecycle.servers.some((item) => item.selected &&
|
|
697
|
+
item.connection_mode === "manual-local" &&
|
|
698
|
+
(item.install !== "ready" || item.snippet === "missing"));
|
|
699
|
+
}
|
|
652
700
|
function dedupeStrings(values) {
|
|
653
701
|
return [...new Set(values.filter(Boolean))];
|
|
654
702
|
}
|
|
@@ -943,10 +991,11 @@ function printSetupHelp() {
|
|
|
943
991
|
console.log([
|
|
944
992
|
"Usage: academic-research setup [options]",
|
|
945
993
|
"",
|
|
946
|
-
"Print project onboarding status and
|
|
994
|
+
"Print project onboarding status and complete safe project-local setup when possible.",
|
|
947
995
|
"",
|
|
948
996
|
"Options:",
|
|
949
997
|
" --root <path> Project root. Default: current directory.",
|
|
998
|
+
" --env-file <path> Read local env values for guided project-local MCP setup.",
|
|
950
999
|
" -h, --help Show this help."
|
|
951
1000
|
].join("\n"));
|
|
952
1001
|
}
|
package/dist/src/project.js
CHANGED
|
@@ -3,7 +3,7 @@ import { copyFile, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promi
|
|
|
3
3
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import YAML from "yaml";
|
|
6
|
-
import { DEFAULT_AGENT, formatMcpDotenv, initializeCapabilities, installSkills, readCapabilities, renderCapabilityProfile, renderMcpSetup, renderMcpSnippet, resolveMcpServerForState, writeMcpEnvironmentExample } from "./capabilities.js";
|
|
6
|
+
import { DEFAULT_AGENT, formatMcpDotenv, initializeCapabilities, installSkills, mcpLocalSetupGitignoreWarning, mcpMissingGeneratedSnippetMessage, readCapabilities, readCapabilityLock, renderCapabilityProfile, renderMcpSetup, renderMcpSnippet, resolveMcpServerForState, writeMcpEnvironmentExample } from "./capabilities.js";
|
|
7
7
|
import { assertKnownAgentTarget } from "./agents.js";
|
|
8
8
|
import { copyDirectory, exists, isNonEmptyDirectory, movePath, readJson, writeJson } from "./files.js";
|
|
9
9
|
import { packageify, slugify, titleFromSlug } from "./names.js";
|
|
@@ -239,6 +239,18 @@ export async function doctorProject(root) {
|
|
|
239
239
|
if (needsMcpEnvDoctor) {
|
|
240
240
|
warnings.push("MCP readiness may require local secrets; run npm run mcp:doctor -- --env-file .env.local");
|
|
241
241
|
}
|
|
242
|
+
const lock = await readCapabilityLock(target);
|
|
243
|
+
const hasManualLocalMcp = state.mcp_servers.some((serverName) => {
|
|
244
|
+
if (!AGENT_STACK.mcp_servers[serverName])
|
|
245
|
+
return false;
|
|
246
|
+
const server = resolveMcpServerForState(state, serverName, state.mcp_server_modes[serverName]);
|
|
247
|
+
return server.connection_mode === "manual-local";
|
|
248
|
+
});
|
|
249
|
+
if (hasManualLocalMcp) {
|
|
250
|
+
const gitignoreWarning = await mcpLocalSetupGitignoreWarning(target);
|
|
251
|
+
if (gitignoreWarning)
|
|
252
|
+
warnings.push(gitignoreWarning);
|
|
253
|
+
}
|
|
242
254
|
for (const serverName of state.mcp_servers) {
|
|
243
255
|
if (!AGENT_STACK.mcp_servers[serverName])
|
|
244
256
|
continue;
|
|
@@ -264,7 +276,16 @@ export async function doctorProject(root) {
|
|
|
264
276
|
const generated = JSON.parse(raw);
|
|
265
277
|
for (const server of snippetServers) {
|
|
266
278
|
if (!Object.hasOwn(generated.mcpServers ?? {}, server)) {
|
|
267
|
-
|
|
279
|
+
const resolved = resolveMcpServerForState(state, server, state.mcp_server_modes[server]);
|
|
280
|
+
errors.push(mcpMissingGeneratedSnippetMessage(server, resolved, process.env));
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const resolved = resolveMcpServerForState(state, server, state.mcp_server_modes[server]);
|
|
284
|
+
if (state.agent === "codex" &&
|
|
285
|
+
resolved.connection_mode === "manual-local" &&
|
|
286
|
+
lock.mcp[server]?.setup?.status === "ready" &&
|
|
287
|
+
lock.mcp[server]?.clients?.codex?.status !== "registered") {
|
|
288
|
+
warnings.push(`${server} is setup locally but not registered in Codex\nNEXT: npm run mcp:client:add -- ${server} --agent codex`);
|
|
268
289
|
}
|
|
269
290
|
}
|
|
270
291
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-academic-research",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"description": "Scaffold agent-ready academic research repositories with SOTA, source ledgers, wiki memory, MCP setup, and project-local skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/template/README.md
CHANGED
|
@@ -111,7 +111,10 @@ git. `mcp doctor` checks the current process environment unless you explicitly
|
|
|
111
111
|
pass `--env-file .env.local`.
|
|
112
112
|
|
|
113
113
|
`setup` prints the current project capability state, installed skill counts,
|
|
114
|
-
selected MCP records, and the next onboarding commands
|
|
114
|
+
selected MCP records, and the next onboarding commands. With
|
|
115
|
+
`-- --env-file .env.local`, it can complete safe project-local setup such as the
|
|
116
|
+
Overleaf wrapper and generated MCP snippet. It does not register global MCP
|
|
117
|
+
clients.
|
|
115
118
|
`mcp smoke` performs a non-launching MCP readiness check: it reports required
|
|
116
119
|
env vars, local/manual setup, and whether client runtime commands such as `uvx`
|
|
117
120
|
or `npx` are available. `mcp probe` is opt-in: local stdio servers get a real
|
|
@@ -69,6 +69,9 @@ credentialed local integrations should use a wrapper that loads `.env.local` at
|
|
|
69
69
|
runtime. Overleaf client registration is intentionally blocked until
|
|
70
70
|
`npm run mcp:setup -- overleaf --mode local --env-file .env.local` has created
|
|
71
71
|
the wrapper and recorded non-secret setup facts.
|
|
72
|
+
After a legacy scaffold update, `npm run setup -- --env-file .env.local` can
|
|
73
|
+
perform that project-local setup and refresh the generated snippet; global
|
|
74
|
+
client registration remains explicit.
|
|
72
75
|
|
|
73
76
|
Custom remote endpoints may use a stored URL or a URL env var name. Bearer token
|
|
74
77
|
support stores only the token env var name. Codex automatic registration
|
|
@@ -70,3 +70,14 @@ npm run mcp:setup -- overleaf --mode local --env-file .env.local
|
|
|
70
70
|
npm run mcp:client:add -- overleaf --agent codex --dry-run
|
|
71
71
|
npm run mcp:probe -- overleaf --env-file .env.local
|
|
72
72
|
```
|
|
73
|
+
|
|
74
|
+
After a scaffold update, the friendlier project setup command can run the same
|
|
75
|
+
project-local Overleaf setup when the env file is present:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm run setup -- --env-file .env.local
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This may create ignored files under `.academic-research/mcp/` and refresh the
|
|
82
|
+
generated MCP snippet. It does not run `codex mcp add`; client registration
|
|
83
|
+
stays explicit.
|
|
@@ -8,7 +8,9 @@ Use this path for the first working session in a new research repository.
|
|
|
8
8
|
npm install
|
|
9
9
|
npm run doctor
|
|
10
10
|
npm run update
|
|
11
|
-
npm run
|
|
11
|
+
npm run update -- --apply
|
|
12
|
+
npm run setup -- --env-file .env.local
|
|
13
|
+
npm run doctor
|
|
12
14
|
```
|
|
13
15
|
|
|
14
16
|
`doctor` checks required files and structural contracts. `update` uses
|
|
@@ -16,7 +18,9 @@ npm run setup
|
|
|
16
18
|
`-- --apply`. Safe scaffold files are tracked in
|
|
17
19
|
`.academic-research/managed-files.json`; locally edited files are skipped
|
|
18
20
|
instead of overwritten. `setup` prints the active skill preset, installed skill
|
|
19
|
-
count, enabled MCP records, and next commands
|
|
21
|
+
count, enabled MCP records, and next commands. With `-- --env-file .env.local`,
|
|
22
|
+
it can complete safe project-local MCP setup such as the Overleaf wrapper and
|
|
23
|
+
generated snippet. It does not register global MCP clients.
|
|
20
24
|
|
|
21
25
|
If an older project still has a pinned `update` script, run:
|
|
22
26
|
|