diff --git a/README.md b/README.md index 29d2f8e..5ee2574 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,8 @@ Of course, you can combine the options, e.g.: Every new workspace is ready for a coding agent such as Claude Code or Codex: - `AGENTS.md` (and a `CLAUDE.md` that imports it) explains how the engine works and where its source, guides and examples live in the workspace; - the `neo-app-authoring` and `neural-link` skills are linked into `.agents/skills/` and `.claude/skills/` by the workspace's own `npm install`; -- each app's `neo-config.json` sets `useAiClient: "development"`, so the app connects to the Neural Link while you build it. +- each app's `neo-config.json` sets `useAiClient: "development"`, so the app connects to the Neural Link while you build it; +- `.mcp.json` (Claude Code) and `.codex/config.toml` (Codex) start the Neural Link MCP server from npm (`npx -y neo-neural-link`). Your client asks once before it trusts a project's servers. Leave all of that out: > npx neo-app --no-agent diff --git a/scripts/test-agent-layer.mjs b/scripts/test-agent-layer.mjs index c00611e..037f799 100644 --- a/scripts/test-agent-layer.mjs +++ b/scripts/test-agent-layer.mjs @@ -9,7 +9,7 @@ import assert from 'node:assert/s import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import createAgentLayer, {AGENT_SKILLS_RANGE, AI_CLIENT_ENVIRONMENT} from '../tasks/createAgentLayer.mjs'; +import createAgentLayer, {AGENT_SKILLS_RANGE, AI_CLIENT_ENVIRONMENT, NEURAL_LINK_PACKAGE} from '../tasks/createAgentLayer.mjs'; import createNeoConfigJson from '../tasks/createNeoConfigJson.mjs'; import createPackageJson from '../tasks/createPackageJson.mjs'; @@ -23,7 +23,10 @@ const fs.mkdirSync(appPath, {recursive: true}); createPackageJson.init('MyApp', workspace, fs, os, path, agent); createNeoConfigJson.init('MyApp', appPath, fs, ['DragDrop', 'Navigator', 'Stylesheet'], os, path, ['all'], 'no', 'no', agent); - agent && createAgentLayer.writeClaudeMd(workspace, fs, path); + if (agent) { + createAgentLayer.writeClaudeMd(workspace, fs, path); + createAgentLayer.writeMcpConfigs(workspace, fs, path); + } return workspace }; @@ -40,6 +43,12 @@ try { assert.deepEqual(Object.keys(packageJson.scripts), [...Object.keys(packageJson.scripts)].sort(), 'and so do the scripts'); assert.equal(read(workspace, 'apps', 'myapp', 'neo-config.json').useAiClient, AI_CLIENT_ENVIRONMENT, 'the app connects to the Neural Link in development'); assert.equal(fs.readFileSync(path.join(workspace, 'CLAUDE.md'), 'utf-8'), '@AGENTS.md\n', 'and CLAUDE.md only imports AGENTS.md'); + + // Project MCP configs start the published Neural Link package; nothing in a user's home is touched. + assert.deepEqual(read(workspace, '.mcp.json'), {mcpServers: {'neural-link': {command: 'npx', args: ['-y', NEURAL_LINK_PACKAGE]}}}, + 'Claude Code starts the Neural Link from npm'); + assert.equal(fs.readFileSync(path.join(workspace, '.codex', 'config.toml'), 'utf-8'), + `[mcp_servers.neural-link]\ncommand = "npx"\nargs = ["-y", "${NEURAL_LINK_PACKAGE}"]\n`, 'and so does Codex'); } // ── --no-agent writes none of it ───────────────────────────────────────────────────────────── @@ -51,6 +60,7 @@ try { assert.equal(packageJson.devDependencies['neo-agent-skills'], undefined, 'no skills dependency'); assert.equal(read(workspace, 'apps', 'myapp', 'neo-config.json').useAiClient, undefined, 'no Neural Link client'); assert.ok(!fs.existsSync(path.join(workspace, 'CLAUDE.md')), 'no CLAUDE.md'); + assert.ok(!fs.existsSync(path.join(workspace, '.mcp.json')) && !fs.existsSync(path.join(workspace, '.codex')), 'and no MCP config'); } // ── --agent-only adds the layer to an existing workspace, and only once ────────────────────── @@ -61,11 +71,13 @@ try { fs.mkdirSync(other, {recursive: true}); fs.writeFileSync(path.join(other, 'neo-config.json'), JSON.stringify({useAiClient: false}, null, 4)); fs.writeFileSync(path.join(workspace, 'README.md'), 'mine\n'); + fs.writeFileSync(path.join(workspace, '.mcp.json'), '{"mcpServers": {}}\n'); const changed = createAgentLayer.addToWorkspace(workspace, fs, os, path); - assert.deepEqual(changed.sort(), ['CLAUDE.md', path.join('apps', 'myapp', 'neo-config.json'), 'package.json'].sort(), - 'it changes the package, the app without the switch, and adds CLAUDE.md'); + assert.deepEqual(changed.sort(), ['.codex/config.toml', 'CLAUDE.md', path.join('apps', 'myapp', 'neo-config.json'), 'package.json'].sort(), + 'it changes the package and the app without the switch, and adds CLAUDE.md and the Codex config'); + assert.equal(fs.readFileSync(path.join(workspace, '.mcp.json'), 'utf-8'), '{"mcpServers": {}}\n', 'a workspace\'s own .mcp.json is never overwritten'); assert.equal(read(other, 'neo-config.json').useAiClient, false, 'an app that already sets useAiClient keeps its choice'); assert.equal(fs.readFileSync(path.join(workspace, 'README.md'), 'utf-8'), 'mine\n', 'and no other file changes'); assert.deepEqual(createAgentLayer.addToWorkspace(workspace, fs, os, path), [], 'a second run changes nothing'); diff --git a/tasks/createAgentLayer.mjs b/tasks/createAgentLayer.mjs index a6cffbd..89501eb 100644 --- a/tasks/createAgentLayer.mjs +++ b/tasks/createAgentLayer.mjs @@ -12,6 +12,9 @@ * so they update with the package. * - `useAiClient` in each app's `neo-config.json`, set to the development environment, so the app connects to the * Neural Link while it is being built and never in a production build. + * - project MCP configs, `.mcp.json` for Claude Code and `.codex/config.toml` for Codex, which start the published + * Neural Link package through `npx`. Never the user-home configs: a scaffold has no business there. Each client + * asks before it trusts a project's servers, and that consent stays the client's. */ /** @@ -26,6 +29,12 @@ export const AGENT_SKILLS_RANGE = '^0.1.61'; */ export const AI_CLIENT_ENVIRONMENT = 'development'; +/** + * @summary The npm package that runs the Neural Link MCP server with no checkout. + * @member {String} + */ +export const NEURAL_LINK_PACKAGE = 'neo-neural-link'; + /** * @summary The scripts the layer adds to a workspace's `package.json`. * @member {Object} @@ -40,6 +49,34 @@ const AGENTS_MD_ARGS = ['--no-install', 'neo-agent-skills-agents-md', '--repo', 'neo-app-workspace', '--audience', 'app', '--out', 'AGENTS.md']; export default { + /** + * @summary Writes the project MCP configs that start the Neural Link, each only where the workspace has none. + * @param {String} workspace + * @param {Object} fs + * @param {Object} path + * @returns {String[]} The files it wrote, relative to the workspace + */ + writeMcpConfigs(workspace, fs, path) { + const + written = [], + files = { + '.mcp.json' : JSON.stringify({mcpServers: {'neural-link': {command: 'npx', args: ['-y', NEURAL_LINK_PACKAGE]}}}, null, 4) + '\n', + '.codex/config.toml': `[mcp_servers.neural-link]\ncommand = "npx"\nargs = ["-y", "${NEURAL_LINK_PACKAGE}"]\n` + }; + + for (const [name, content] of Object.entries(files)) { + const file = path.join(workspace, name); + + if (!fs.existsSync(file)) { + fs.mkdirSync(path.dirname(file), {recursive: true}); + fs.writeFileSync(file, content); + written.push(name) + } + } + + return written + }, + /** * @summary Writes `CLAUDE.md` into a workspace that has none. * @param {String} workspace @@ -129,6 +166,8 @@ export default { changed.push('CLAUDE.md') } + changed.push(...this.writeMcpConfigs(workspace, fs, path)); + return changed } }; diff --git a/tasks/neo-app.mjs b/tasks/neo-app.mjs index e128670..0f02e43 100755 --- a/tasks/neo-app.mjs +++ b/tasks/neo-app.mjs @@ -194,7 +194,10 @@ if (programOpts.info) { createServiceWorker.init(appName, workspace, fs, os, path); createSrcFolder .init(path.join(cwd, workspace), fs, os, path, __dirname); - programOpts.agent && createAgentLayer.writeClaudeMd(workspace, fs, path); + if (programOpts.agent) { + createAgentLayer.writeClaudeMd(workspace, fs, path); + createAgentLayer.writeMcpConfigs(workspace, fs, path); + } fs.copySync( path.join(__dirname, '../resources/copyExamples.mjs'),