agent-skills-standard 2.1.2 → 2.2.0

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/dist/index.js CHANGED
@@ -9,6 +9,7 @@ const path = require("path");
9
9
  const yaml = require("js-yaml");
10
10
  const zod = require("zod");
11
11
  const fetch$1 = require("cross-fetch");
12
+ const os = require("os");
12
13
  const child_process = require("child_process");
13
14
  class FeedbackService {
14
15
  /**
@@ -838,6 +839,13 @@ const SUPPORTED_AGENTS = Object.values(Agent).map(getAgentDefinition);
838
839
  const SUPPORTED_FRAMEWORKS = Object.values(
839
840
  Framework
840
841
  ).map(getFrameworkDefinition);
842
+ const McpScopeSchema = zod.z.enum(["project", "user", "snippets-only", "disabled"]);
843
+ const McpConfigSchema = zod.z.object({
844
+ enabled: zod.z.boolean(),
845
+ scope: McpScopeSchema,
846
+ prompted: zod.z.boolean(),
847
+ version: zod.z.string().optional()
848
+ });
841
849
  const SkillConfigSchema = zod.z.object({
842
850
  registry: zod.z.string().url(),
843
851
  agents: zod.z.array(zod.z.nativeEnum(Agent)).optional(),
@@ -851,7 +859,8 @@ const SkillConfigSchema = zod.z.object({
851
859
  })
852
860
  ),
853
861
  custom_overrides: zod.z.array(zod.z.string()).optional(),
854
- workflows: zod.z.union([zod.z.boolean(), zod.z.array(zod.z.string())]).optional()
862
+ workflows: zod.z.union([zod.z.boolean(), zod.z.array(zod.z.string())]).optional(),
863
+ mcp: McpConfigSchema.optional()
855
864
  });
856
865
  class ConfigService {
857
866
  /**
@@ -1369,6 +1378,13 @@ class InitService {
1369
1378
  const projectDeps = await this.detectionService.getProjectDeps();
1370
1379
  this.configService.applyDependencyExclusions(config, projectDeps);
1371
1380
  this.configService.reconcileDependencies(config, projectDeps);
1381
+ if (answers.mcpScope !== void 0) {
1382
+ config.mcp = {
1383
+ enabled: answers.mcpEnabled === true,
1384
+ scope: answers.mcpEnabled === false ? "disabled" : answers.mcpScope,
1385
+ prompted: true
1386
+ };
1387
+ }
1372
1388
  const commentHeader = `# Auto-detected configuration generated by agent-skills-standard init
1373
1389
  #
1374
1390
  # Presence in 'skills' list = Active. To disable a skill, remove it from the list.
@@ -1669,15 +1685,70 @@ class InitCommand {
1669
1685
  default: DEFAULT_REGISTER
1670
1686
  }
1671
1687
  ]);
1688
+ this.printMcpValueProp();
1689
+ const mcpDecision = await inquirer.prompt([
1690
+ {
1691
+ type: "confirm",
1692
+ name: "mcpEnabled",
1693
+ message: "Enable MCP server integration?",
1694
+ default: true
1695
+ },
1696
+ {
1697
+ type: "list",
1698
+ name: "mcpScope",
1699
+ message: "Where should sync write MCP configs?",
1700
+ when: (a) => a.mcpEnabled === true,
1701
+ choices: [
1702
+ {
1703
+ name: "project (recommended) — only files inside this project (.mcp.json, .cursor/mcp.json, etc.)",
1704
+ value: "project"
1705
+ },
1706
+ {
1707
+ name: "user — also $HOME files (~/.cursor/mcp.json, ~/.gemini/settings.json) — sync prompts each",
1708
+ value: "user"
1709
+ },
1710
+ {
1711
+ name: "snippets-only — never edits any runtime config; just generates ./mcp-config-snippets/*.json",
1712
+ value: "snippets-only"
1713
+ }
1714
+ ],
1715
+ default: "project"
1716
+ }
1717
+ ]);
1718
+ answers.mcpEnabled = mcpDecision.mcpEnabled;
1719
+ answers.mcpScope = mcpDecision.mcpEnabled ? mcpDecision.mcpScope : "disabled";
1672
1720
  await this.initService.buildAndSaveConfig(answers, metadata);
1673
1721
  console.log(pc.green("\n✅ Initialized .skillsrc with your preferences!"));
1674
1722
  console.log(pc.gray(` Selected framework: ${answers.framework}`));
1723
+ if (answers.mcpEnabled) {
1724
+ console.log(pc.gray(` MCP integration: enabled (scope: ${answers.mcpScope})`));
1725
+ } else {
1726
+ console.log(pc.gray(` MCP integration: disabled (change later via \`ags mcp enable\`)`));
1727
+ }
1675
1728
  console.log(
1676
1729
  pc.cyan(
1677
- "\nNext step: Run `agent-skills-standard sync` to generate rule files."
1730
+ "\nNext step: Run `agent-skills-standard sync` to install skills and wire MCP."
1678
1731
  )
1679
1732
  );
1680
1733
  }
1734
+ printMcpValueProp() {
1735
+ console.log(pc.bold("\n🔌 MCP server setup\n"));
1736
+ console.log(
1737
+ "agent-skills-standard ships an optional MCP server that serves your skills"
1738
+ );
1739
+ console.log("to AI agents at runtime. Quick comparison:\n");
1740
+ console.log(pc.bold(" WITHOUT MCP") + " (skills as static files only)");
1741
+ console.log(pc.green(" ✓") + " Zero setup overhead");
1742
+ console.log(pc.red(" ✗") + " Sub-agents (Claude Code tdd-implementer, architecture-guard, etc.)");
1743
+ console.log(" do NOT inherit AGENTS.md — they often skip skill loading entirely");
1744
+ console.log(pc.red(" ✗") + " No audit trail of which skills were consulted");
1745
+ console.log(pc.red(" ✗") + " False findings in code review (agent uses pre-training defaults)\n");
1746
+ console.log(pc.bold(" WITH MCP") + " (runtime tool calls)");
1747
+ console.log(pc.green(" ✓") + " Sub-agents in EVERY runtime can call load_skills_for_files");
1748
+ console.log(pc.green(" ✓") + " Returns matched SKILL.md as a tool response — can't be silently skipped");
1749
+ console.log(pc.green(" ✓") + " audit_session_compliance gives you a receipt of what was loaded");
1750
+ console.log(pc.gray(" -") + " Adds one more process to your AI runtime config\n");
1751
+ }
1681
1752
  }
1682
1753
  class SkillService {
1683
1754
  registryService = new RegistryService();
@@ -1793,9 +1864,588 @@ Available skills for ${framework}:`));
1793
1864
  );
1794
1865
  }
1795
1866
  }
1867
+ const SERVER_NAME = "agent-skills-standard";
1868
+ const PACKAGE = "agent-skills-standard-mcp";
1869
+ const getTargets = (home = os.homedir()) => {
1870
+ const HOME = home;
1871
+ return {
1872
+ [Agent.Claude]: {
1873
+ agent: Agent.Claude,
1874
+ projectFile: ".mcp.json",
1875
+ userFile: path.join(HOME, ".claude", ".mcp.json"),
1876
+ key: "mcpServers",
1877
+ shape: "map"
1878
+ },
1879
+ [Agent.Cursor]: {
1880
+ agent: Agent.Cursor,
1881
+ projectFile: ".cursor/mcp.json",
1882
+ userFile: path.join(HOME, ".cursor", "mcp.json"),
1883
+ key: "mcpServers",
1884
+ shape: "map"
1885
+ },
1886
+ [Agent.Antigravity]: {
1887
+ agent: Agent.Antigravity,
1888
+ projectFile: ".antigravity/mcp.json",
1889
+ userFile: null,
1890
+ key: "mcpServers",
1891
+ shape: "map"
1892
+ },
1893
+ [Agent.Kiro]: {
1894
+ agent: Agent.Kiro,
1895
+ projectFile: ".kiro/settings/mcp.json",
1896
+ userFile: null,
1897
+ key: "mcpServers",
1898
+ shape: "map"
1899
+ },
1900
+ [Agent.Windsurf]: {
1901
+ agent: Agent.Windsurf,
1902
+ projectFile: ".codeium/windsurf/mcp_config.json",
1903
+ userFile: path.join(HOME, ".codeium", "windsurf", "mcp_config.json"),
1904
+ key: "mcpServers",
1905
+ shape: "map"
1906
+ },
1907
+ [Agent.Trae]: {
1908
+ agent: Agent.Trae,
1909
+ projectFile: ".trae/mcp.json",
1910
+ userFile: null,
1911
+ key: "mcpServers",
1912
+ shape: "map"
1913
+ },
1914
+ [Agent.Roo]: {
1915
+ agent: Agent.Roo,
1916
+ projectFile: ".roo/mcp.json",
1917
+ userFile: null,
1918
+ key: "mcpServers",
1919
+ shape: "map"
1920
+ },
1921
+ [Agent.Gemini]: {
1922
+ agent: Agent.Gemini,
1923
+ projectFile: ".gemini/settings.json",
1924
+ userFile: path.join(HOME, ".gemini", "settings.json"),
1925
+ key: "mcpServers",
1926
+ shape: "map"
1927
+ }
1928
+ };
1929
+ };
1930
+ class McpConfigService {
1931
+ /** Returns the standard MCP server entry our CLI proposes. */
1932
+ buildEntry(version2) {
1933
+ const spec = version2 ? `${PACKAGE}@${version2}` : PACKAGE;
1934
+ return { command: "npx", args: ["-y", spec] };
1935
+ }
1936
+ /** For testing: allows overriding the home directory. */
1937
+ testHome = null;
1938
+ setHomeForTesting(home) {
1939
+ this.testHome = home;
1940
+ }
1941
+ getTargets() {
1942
+ return getTargets(this.testHome ?? void 0);
1943
+ }
1944
+ /**
1945
+ * Write MCP configs for the given agents based on the configured scope.
1946
+ * Always safe-merge (never overwrite other servers, never replace whole files).
1947
+ */
1948
+ async install(opts) {
1949
+ const report = {
1950
+ projectWrites: [],
1951
+ userWrites: [],
1952
+ snippets: [],
1953
+ declined: [],
1954
+ unsupported: []
1955
+ };
1956
+ const { rootDir, agents, mcp } = opts;
1957
+ if (mcp.scope === "disabled" || !mcp.enabled) {
1958
+ return report;
1959
+ }
1960
+ const TARGETS = this.getTargets();
1961
+ const entry = this.buildEntry(mcp.version);
1962
+ await this.generateSnippets(rootDir, agents, entry, report);
1963
+ if (mcp.scope === "snippets-only") {
1964
+ return report;
1965
+ }
1966
+ for (const agent of agents) {
1967
+ const target = TARGETS[agent];
1968
+ if (!target) {
1969
+ report.unsupported.push(agent);
1970
+ continue;
1971
+ }
1972
+ if (target.projectFile) {
1973
+ const abs = path.join(rootDir, target.projectFile);
1974
+ const action = await this.mergeFile(abs, target, entry);
1975
+ report.projectWrites.push({ agent, file: target.projectFile, action });
1976
+ }
1977
+ if (mcp.scope === "user" && target.userFile) {
1978
+ const ok = opts.userScopePrompt ? await opts.userScopePrompt(agent, target.userFile) : false;
1979
+ if (!ok) {
1980
+ report.declined.push({ agent, file: target.userFile });
1981
+ continue;
1982
+ }
1983
+ const action = await this.mergeFile(target.userFile, target, entry);
1984
+ report.userWrites.push({ agent, file: target.userFile, action });
1985
+ }
1986
+ }
1987
+ return report;
1988
+ }
1989
+ /**
1990
+ * Remove our MCP entry from configs we may have added it to. Other entries
1991
+ * in the same file are untouched.
1992
+ */
1993
+ async uninstall(opts) {
1994
+ const removed = [];
1995
+ const TARGETS = this.getTargets();
1996
+ for (const agent of opts.agents) {
1997
+ const target = TARGETS[agent];
1998
+ if (!target) continue;
1999
+ const candidates = [];
2000
+ if (target.projectFile && (opts.from === "project" || opts.from === "all")) {
2001
+ candidates.push({
2002
+ abs: path.join(opts.rootDir, target.projectFile),
2003
+ rel: target.projectFile
2004
+ });
2005
+ }
2006
+ if (target.userFile && (opts.from === "user" || opts.from === "all")) {
2007
+ candidates.push({ abs: target.userFile, rel: target.userFile });
2008
+ }
2009
+ for (const { abs, rel } of candidates) {
2010
+ if (!await fs.pathExists(abs)) continue;
2011
+ const removedHere = await this.removeFromFile(abs, target);
2012
+ if (removedHere) removed.push({ agent, file: rel });
2013
+ }
2014
+ }
2015
+ return { removed };
2016
+ }
2017
+ /** Snapshot what's currently installed. Read-only. */
2018
+ async status(opts) {
2019
+ const out = [];
2020
+ const TARGETS = this.getTargets();
2021
+ for (const agent of opts.agents) {
2022
+ const target = TARGETS[agent];
2023
+ if (!target) continue;
2024
+ const row = {
2025
+ agent
2026
+ };
2027
+ if (target.projectFile) {
2028
+ row.project = await this.hasOurEntry(
2029
+ path.join(opts.rootDir, target.projectFile),
2030
+ target
2031
+ );
2032
+ }
2033
+ if (target.userFile) {
2034
+ row.user = await this.hasOurEntry(target.userFile, target);
2035
+ }
2036
+ out.push(row);
2037
+ }
2038
+ return out;
2039
+ }
2040
+ // ---- private helpers ----
2041
+ async generateSnippets(rootDir, agents, entry, report) {
2042
+ const dir = path.join(rootDir, "mcp-config-snippets");
2043
+ await fs.ensureDir(dir);
2044
+ const TARGETS = this.getTargets();
2045
+ for (const agent of agents) {
2046
+ const target = TARGETS[agent];
2047
+ if (!target) continue;
2048
+ const file = path.join(dir, `${agent}.json`);
2049
+ const snippet = this.buildFreshDoc(target, entry);
2050
+ await fs.writeJson(file, snippet, { spaces: 2 });
2051
+ report.snippets.push({ agent, file: path.relative(rootDir, file) });
2052
+ }
2053
+ }
2054
+ /**
2055
+ * Read-modify-write a config file. Only the SERVER_NAME key is touched.
2056
+ * Never replaces or removes other entries.
2057
+ */
2058
+ async mergeFile(abs, target, entry) {
2059
+ const existing = await fs.pathExists(abs) ? await fs.readJson(abs).catch(() => ({})) : {};
2060
+ const container = this.ensurePathContainer(existing, target);
2061
+ if (target.shape === "map") {
2062
+ const map = container;
2063
+ const previous = map[SERVER_NAME];
2064
+ const next = entry;
2065
+ if (previous && deepEqual(previous, next)) {
2066
+ return "skipped-existing";
2067
+ }
2068
+ map[SERVER_NAME] = next;
2069
+ await this.writeAtomic(abs, existing);
2070
+ return previous ? "updated" : "added";
2071
+ } else {
2072
+ const list = container;
2073
+ const idx = list.findIndex((item) => item.name === SERVER_NAME);
2074
+ const next = {
2075
+ name: SERVER_NAME,
2076
+ transport: { type: "stdio", ...entry }
2077
+ };
2078
+ if (idx >= 0) {
2079
+ if (deepEqual(list[idx], next)) return "skipped-existing";
2080
+ list[idx] = next;
2081
+ await this.writeAtomic(abs, existing);
2082
+ return "updated";
2083
+ }
2084
+ list.push(next);
2085
+ await this.writeAtomic(abs, existing);
2086
+ return "added";
2087
+ }
2088
+ }
2089
+ async removeFromFile(abs, target) {
2090
+ const data = await fs.readJson(abs).catch(() => null);
2091
+ if (!data) return false;
2092
+ const container = this.ensurePathContainer(data, target);
2093
+ if (target.shape === "map") {
2094
+ const map = container;
2095
+ if (!(SERVER_NAME in map)) return false;
2096
+ delete map[SERVER_NAME];
2097
+ await this.writeAtomic(abs, data);
2098
+ return true;
2099
+ } else {
2100
+ const list = container;
2101
+ const before = list.length;
2102
+ const filtered = list.filter((item) => item.name !== SERVER_NAME);
2103
+ if (filtered.length === before) return false;
2104
+ this.setNestedValue(data, target.key, filtered);
2105
+ await this.writeAtomic(abs, data);
2106
+ return true;
2107
+ }
2108
+ }
2109
+ async hasOurEntry(abs, target) {
2110
+ if (!await fs.pathExists(abs)) return false;
2111
+ const data = await fs.readJson(abs).catch(() => null);
2112
+ if (!data) return false;
2113
+ const container = this.getNestedValue(data, target.key);
2114
+ if (!container) return false;
2115
+ if (target.shape === "map") {
2116
+ return Boolean(container[SERVER_NAME]);
2117
+ }
2118
+ return container.some(
2119
+ (item) => item.name === SERVER_NAME
2120
+ );
2121
+ }
2122
+ buildFreshDoc(target, entry) {
2123
+ const doc = {};
2124
+ const container = target.shape === "map" ? { [SERVER_NAME]: entry } : [{ name: SERVER_NAME, transport: { type: "stdio", ...entry } }];
2125
+ this.setNestedValue(doc, target.key, container);
2126
+ return doc;
2127
+ }
2128
+ ensurePathContainer(data, target) {
2129
+ const existing = this.getNestedValue(data, target.key);
2130
+ if (existing) {
2131
+ if (target.shape === "map") {
2132
+ if (typeof existing === "object" && existing !== null && !Array.isArray(existing)) {
2133
+ return existing;
2134
+ }
2135
+ } else if (target.shape === "list") {
2136
+ if (Array.isArray(existing)) {
2137
+ return existing;
2138
+ }
2139
+ }
2140
+ }
2141
+ const fresh = target.shape === "map" ? {} : [];
2142
+ this.setNestedValue(data, target.key, fresh);
2143
+ return fresh;
2144
+ }
2145
+ getNestedValue(data, dotted) {
2146
+ const parts = dotted.split(".");
2147
+ let cur = data;
2148
+ for (const p of parts) {
2149
+ if (p === "__proto__" || p === "constructor" || p === "prototype") {
2150
+ throw new Error(
2151
+ `Prototype pollution attempt detected in key: ${dotted}`
2152
+ );
2153
+ }
2154
+ if (typeof cur !== "object" || cur === null) return void 0;
2155
+ cur = cur[p];
2156
+ }
2157
+ return cur;
2158
+ }
2159
+ setNestedValue(data, dotted, value) {
2160
+ const parts = dotted.split(".");
2161
+ let cur = data;
2162
+ for (let i = 0; i < parts.length - 1; i++) {
2163
+ const p = parts[i];
2164
+ if (p === "__proto__" || p === "constructor" || p === "prototype") {
2165
+ throw new Error(
2166
+ `Prototype pollution attempt detected in key: ${dotted}`
2167
+ );
2168
+ }
2169
+ if (typeof cur[p] !== "object" || cur[p] === null) {
2170
+ cur[p] = {};
2171
+ }
2172
+ cur = cur[p];
2173
+ }
2174
+ const last = parts[parts.length - 1];
2175
+ if (last === "__proto__" || last === "constructor" || last === "prototype") {
2176
+ throw new Error(`Prototype pollution attempt detected in key: ${dotted}`);
2177
+ }
2178
+ cur[last] = value;
2179
+ }
2180
+ async writeAtomic(abs, data) {
2181
+ await fs.ensureDir(path.dirname(abs));
2182
+ const tmp = `${abs}.tmp`;
2183
+ await fs.writeJson(tmp, data, { spaces: 2 });
2184
+ await fs.move(tmp, abs, { overwrite: true });
2185
+ }
2186
+ }
2187
+ function defaultMcpConfig() {
2188
+ return { enabled: false, scope: "snippets-only", prompted: false };
2189
+ }
2190
+ function deepEqual(a, b) {
2191
+ return JSON.stringify(a) === JSON.stringify(b);
2192
+ }
2193
+ const VALID_SCOPES = ["project", "user", "snippets-only", "disabled"];
2194
+ class McpCommand {
2195
+ configService;
2196
+ mcpService;
2197
+ constructor(configService, mcpService) {
2198
+ this.configService = configService ?? new ConfigService();
2199
+ this.mcpService = mcpService ?? new McpConfigService();
2200
+ }
2201
+ async run(action, options = {}) {
2202
+ const config = await this.configService.loadConfig();
2203
+ if (!config) {
2204
+ console.log(
2205
+ pc.red(
2206
+ "❌ .skillsrc not found. Run `agent-skills-standard init` first."
2207
+ )
2208
+ );
2209
+ return;
2210
+ }
2211
+ const cwd = process.cwd();
2212
+ const agents = config.agents ?? [];
2213
+ switch (action) {
2214
+ case "status":
2215
+ return this.actionStatus(config, cwd);
2216
+ case "enable":
2217
+ return this.actionEnable(config);
2218
+ case "disable":
2219
+ return this.actionDisable(config);
2220
+ case "scope":
2221
+ return this.actionScope(config, options);
2222
+ case "install":
2223
+ return this.actionInstall(config, agents, cwd, options);
2224
+ case "uninstall":
2225
+ return this.actionUninstall(config, agents, cwd, options);
2226
+ case "snippets":
2227
+ return this.actionSnippets(config, agents, cwd);
2228
+ default:
2229
+ console.log(
2230
+ pc.red(`Unknown action: ${action}`),
2231
+ "\nValid actions: status, enable, disable, scope, install, uninstall, snippets"
2232
+ );
2233
+ }
2234
+ }
2235
+ // ---- actions ----
2236
+ async actionStatus(config, cwd) {
2237
+ const mcp = config.mcp ?? defaultMcpConfig();
2238
+ const agents = config.agents ?? [];
2239
+ const rows = await this.mcpService.status({ rootDir: cwd, agents });
2240
+ console.log(pc.bold("\n🔌 MCP integration status\n"));
2241
+ console.log(` enabled : ${mcp.enabled ? pc.green("yes") : pc.gray("no")}`);
2242
+ console.log(` scope : ${pc.cyan(mcp.scope)}`);
2243
+ console.log(` prompted: ${mcp.prompted ? "yes" : "no"}`);
2244
+ if (mcp.version) console.log(` version : ${mcp.version}`);
2245
+ if (rows.length === 0) {
2246
+ console.log(pc.gray("\n (no agents configured)"));
2247
+ return;
2248
+ }
2249
+ console.log("\n Per-agent installation state:");
2250
+ let installedAnywhere = false;
2251
+ for (const row of rows) {
2252
+ const project = row.project === void 0 ? "—" : row.project ? pc.green("✓") : pc.gray("✗");
2253
+ const user = row.user === void 0 ? "—" : row.user ? pc.green("✓") : pc.gray("✗");
2254
+ if (row.project === true || row.user === true) installedAnywhere = true;
2255
+ console.log(` ${row.agent.padEnd(12)} project: ${project} user: ${user}`);
2256
+ }
2257
+ if (!mcp.enabled && installedAnywhere) {
2258
+ console.log(
2259
+ pc.yellow(
2260
+ "\n⚠️ Mismatch detected: .skillsrc says mcp.enabled=false, but the MCP entry IS present in at least one runtime config."
2261
+ )
2262
+ );
2263
+ console.log(
2264
+ pc.gray(
2265
+ " The MCP will work, but `sync` will not keep its config in step. Consider one of:"
2266
+ )
2267
+ );
2268
+ console.log(
2269
+ pc.gray(
2270
+ " • `ags mcp enable` — opt the project in (CLI keeps configs in step)"
2271
+ )
2272
+ );
2273
+ console.log(
2274
+ pc.gray(
2275
+ " • `ags mcp uninstall --from project` — remove the manual entry if it was unintended"
2276
+ )
2277
+ );
2278
+ } else if (mcp.enabled && !installedAnywhere) {
2279
+ console.log(
2280
+ pc.yellow(
2281
+ "\n⚠️ Mismatch detected: .skillsrc says mcp.enabled=true, but no runtime config has the MCP registered."
2282
+ )
2283
+ );
2284
+ console.log(
2285
+ pc.gray(
2286
+ " Run `ags mcp install` to write the entries, or `ags mcp disable` to clear the consent flag."
2287
+ )
2288
+ );
2289
+ }
2290
+ }
2291
+ async actionEnable(config) {
2292
+ const mcp = config.mcp ?? defaultMcpConfig();
2293
+ if (mcp.scope === "disabled") {
2294
+ console.log(
2295
+ pc.yellow(
2296
+ '⚠️ scope is currently "disabled". Set a real scope first: `ags mcp scope project`'
2297
+ )
2298
+ );
2299
+ return;
2300
+ }
2301
+ config.mcp = { ...mcp, enabled: true, prompted: true };
2302
+ await this.configService.saveConfig(config);
2303
+ console.log(pc.green("✅ MCP enabled."));
2304
+ console.log(pc.gray(" Run `ags mcp install` to write configs now, or it happens on next `sync`."));
2305
+ }
2306
+ async actionDisable(config) {
2307
+ const mcp = config.mcp ?? defaultMcpConfig();
2308
+ config.mcp = { ...mcp, enabled: false, prompted: true };
2309
+ await this.configService.saveConfig(config);
2310
+ console.log(pc.yellow("🔕 MCP disabled."));
2311
+ console.log(pc.gray(" Existing config entries were NOT removed. Use `ags mcp uninstall` to clean them."));
2312
+ }
2313
+ async actionScope(config, options) {
2314
+ const scope = options.scope ?? options._?.[0];
2315
+ if (!scope || !VALID_SCOPES.includes(scope)) {
2316
+ console.log(
2317
+ pc.red(`Invalid scope. Valid: ${VALID_SCOPES.join(" | ")}`)
2318
+ );
2319
+ return;
2320
+ }
2321
+ const mcp = config.mcp ?? defaultMcpConfig();
2322
+ config.mcp = { ...mcp, scope, prompted: true };
2323
+ await this.configService.saveConfig(config);
2324
+ console.log(pc.green(`✅ MCP scope set to "${scope}".`));
2325
+ if (scope === "user") {
2326
+ console.log(
2327
+ pc.yellow(
2328
+ "⚠️ user-scope writes will be prompted per file during `sync` or `install`."
2329
+ )
2330
+ );
2331
+ }
2332
+ }
2333
+ async actionInstall(config, agents, cwd, options) {
2334
+ const mcp = config.mcp ?? defaultMcpConfig();
2335
+ const scopeOverride = options.scope;
2336
+ const effectiveScope = scopeOverride ?? mcp.scope;
2337
+ if (scopeOverride && !VALID_SCOPES.includes(scopeOverride)) {
2338
+ console.log(
2339
+ pc.red(`Invalid --scope value. Valid: ${VALID_SCOPES.join(" | ")}`)
2340
+ );
2341
+ return;
2342
+ }
2343
+ if (effectiveScope === "disabled") {
2344
+ console.log(pc.yellow('Scope is "disabled" — nothing to install.'));
2345
+ return;
2346
+ }
2347
+ const userPrompt = async (agent, file) => {
2348
+ const { ok } = await inquirer.prompt([
2349
+ {
2350
+ type: "confirm",
2351
+ name: "ok",
2352
+ message: `Write user-scope MCP config for ${pc.cyan(agent)} at ${pc.gray(file)}?`,
2353
+ default: false
2354
+ }
2355
+ ]);
2356
+ return ok;
2357
+ };
2358
+ const report = await this.mcpService.install({
2359
+ rootDir: cwd,
2360
+ agents,
2361
+ mcp: { ...mcp, enabled: true, scope: effectiveScope },
2362
+ userScopePrompt: userPrompt
2363
+ });
2364
+ this.printReport(report, cwd);
2365
+ }
2366
+ async actionUninstall(config, agents, cwd, options) {
2367
+ const from = options.from ?? "project";
2368
+ if (!["project", "user", "all"].includes(from)) {
2369
+ console.log(pc.red("--from must be: project | user | all"));
2370
+ return;
2371
+ }
2372
+ const { removed } = await this.mcpService.uninstall({
2373
+ rootDir: cwd,
2374
+ agents,
2375
+ from
2376
+ });
2377
+ if (removed.length === 0) {
2378
+ console.log(pc.gray(`Nothing to remove at scope "${from}".`));
2379
+ return;
2380
+ }
2381
+ console.log(pc.green(`✅ Removed agent-skills MCP entry from:`));
2382
+ for (const r of removed) {
2383
+ console.log(` - ${pc.cyan(r.agent)} → ${pc.gray(r.file)}`);
2384
+ }
2385
+ }
2386
+ async actionSnippets(config, agents, cwd) {
2387
+ const mcp = config.mcp ?? defaultMcpConfig();
2388
+ const report = await this.mcpService.install({
2389
+ rootDir: cwd,
2390
+ agents,
2391
+ mcp: { ...mcp, enabled: true, scope: "snippets-only" }
2392
+ });
2393
+ if (report.snippets.length === 0) {
2394
+ console.log(pc.gray("No snippets generated (no supported agents in .skillsrc)."));
2395
+ return;
2396
+ }
2397
+ console.log(pc.green(`✅ Generated ${report.snippets.length} snippet(s):`));
2398
+ for (const s of report.snippets) {
2399
+ console.log(` - ${pc.cyan(s.agent)} → ${pc.gray(s.file)}`);
2400
+ }
2401
+ }
2402
+ // ---- output ----
2403
+ printReport(report, cwd) {
2404
+ if (report.projectWrites.length > 0) {
2405
+ console.log(pc.bold("\n📁 Project-scope writes:"));
2406
+ for (const w of report.projectWrites) {
2407
+ const tag = w.action === "added" ? pc.green("+ added ") : w.action === "updated" ? pc.cyan("~ updated ") : pc.gray("= up-to-date");
2408
+ console.log(` ${tag} ${w.agent.padEnd(12)} ${pc.gray(w.file)}`);
2409
+ }
2410
+ }
2411
+ if (report.userWrites.length > 0) {
2412
+ console.log(pc.bold("\n🏠 User-scope writes:"));
2413
+ for (const w of report.userWrites) {
2414
+ console.log(` ${pc.green("+ wrote ")} ${w.agent.padEnd(12)} ${pc.gray(path.relative(cwd, w.file))}`);
2415
+ }
2416
+ }
2417
+ if (report.declined.length > 0) {
2418
+ console.log(pc.bold("\n⏭ User-scope declined:"));
2419
+ for (const d of report.declined) {
2420
+ console.log(` ${pc.gray("-")} ${d.agent.padEnd(12)} ${pc.gray(d.file)}`);
2421
+ }
2422
+ }
2423
+ if (report.snippets.length > 0) {
2424
+ console.log(pc.bold("\n📄 Snippet files:"));
2425
+ for (const s of report.snippets) {
2426
+ console.log(` ${pc.gray(s.file)}`);
2427
+ }
2428
+ }
2429
+ if (report.unsupported.length > 0) {
2430
+ console.log(
2431
+ pc.gray(
2432
+ `
2433
+ (skipped — no MCP support yet: ${report.unsupported.join(", ")})`
2434
+ )
2435
+ );
2436
+ }
2437
+ }
2438
+ }
1796
2439
  class AgentBridgeService {
1797
2440
  /**
1798
2441
  * Bridges native agent rule files to AGENTS.md by creating discovery instructions.
2442
+ *
2443
+ * The MCP runtime-enforcement block lives in `AGENTS.md` (single source of
2444
+ * truth, generated by `IndexGeneratorService.assembleRouterIndex`). Rule
2445
+ * files generated here just instruct the agent to read AGENTS.md first —
2446
+ * they do NOT duplicate the MCP block. This keeps the message in one place
2447
+ * so it can't drift between surfaces.
2448
+ *
1799
2449
  * @param rootDir Project root directory
1800
2450
  * @param agents List of agents to generate rules for
1801
2451
  */
@@ -1885,7 +2535,7 @@ applyTo: '**/*'
1885
2535
  "",
1886
2536
  "## Agent Protocol",
1887
2537
  "",
1888
- "See `AGENTS.md` for the Zero-Trust skill loading protocol (applies to all AI agents).",
2538
+ "See `AGENTS.md` for the Zero-Trust skill loading protocol (applies to all AI agents) and the MCP runtime-enforcement section (when enabled).",
1889
2539
  "",
1890
2540
  "## Self-Learning Protocol",
1891
2541
  "",
@@ -2354,7 +3004,7 @@ class IndexGeneratorServiceImpl extends IndexGeneratorService {
2354
3004
  );
2355
3005
  return lines.join("\n");
2356
3006
  }
2357
- async assembleRouterIndex(baseDir, allowedCategories) {
3007
+ async assembleRouterIndex(baseDir, allowedCategories, mcpEnabled = false) {
2358
3008
  const fileRouting = await this.reader.loadFileRouting(baseDir);
2359
3009
  let availableCategories = [];
2360
3010
  if (await fs.pathExists(baseDir)) {
@@ -2403,12 +3053,35 @@ class IndexGeneratorServiceImpl extends IndexGeneratorService {
2403
3053
  "| QE workflow | `<SKILLS>/quality-engineering/_INDEX.md` |"
2404
3054
  );
2405
3055
  }
3056
+ const mcpStatusNote = mcpEnabled ? "> [!TIP] **This project has the MCP server enabled in `.skillsrc`** — `sync` keeps your runtime configs in step. Run `ags mcp status` to verify per-agent installation." : "> [!NOTE] To enable MCP-managed installs in this project, run `ags mcp enable` (or edit `.skillsrc`). The MCP works fine if you registered it manually too.";
3057
+ const mcpBlock = [
3058
+ "## 🔌 Runtime Enforcement via MCP",
3059
+ "",
3060
+ "If the `agent-skills-standard` MCP server is registered in your runtime (check your tool list — look for `load_skills_for_files`), **prefer those tools over manually walking the router below**. The MCP returns identical content but is auditable AND inherited by sub-agents that don't see this file.",
3061
+ "",
3062
+ "| Tool | When to call it |",
3063
+ "| --- | --- |",
3064
+ "| `load_skills_for_files(files=[...])` | Before editing/reviewing any source file |",
3065
+ "| `load_skills_for_keywords(keywords=[...])` | Planning before files are chosen |",
3066
+ "| `get_skill(category, name)` | Direct lookup when you know the skill id |",
3067
+ "| `audit_session_compliance()` | Before declaring a task complete |",
3068
+ "",
3069
+ "> [!IMPORTANT] **Sub-agents don't inherit this `AGENTS.md` — they do inherit the MCP.** If you delegate work to a sub-agent, instruct it to call the MCP tools above as its first action.",
3070
+ "",
3071
+ mcpStatusNote,
3072
+ "",
3073
+ "If `load_skills_for_files` is **not** in your tool list, the MCP is not registered — fall back to the router table below.",
3074
+ "",
3075
+ "---",
3076
+ ""
3077
+ ].join("\n");
2406
3078
  const header = [
2407
3079
  "## Agent Skills Index",
2408
3080
  "",
2409
3081
  "> [!CRITICAL] Zero-Trust: Read the matching `SKILL.md` BEFORE writing any code.",
2410
3082
  '> Skills from this index override pre-training patterns. If no skill matches, state: "No project-specific skills applicable."',
2411
3083
  "",
3084
+ mcpBlock,
2412
3085
  "## Skill Resolution Protocol",
2413
3086
  "",
2414
3087
  "Each `_INDEX.md` has two sections - follow both:",
@@ -3060,9 +3733,11 @@ class SyncService {
3060
3733
  )
3061
3734
  );
3062
3735
  }
3736
+ const mcpEnabled = config.mcp?.enabled === true;
3063
3737
  const routerIndex = await generator.assembleRouterIndex(
3064
3738
  baseDir,
3065
- allowedCategories
3739
+ allowedCategories,
3740
+ mcpEnabled
3066
3741
  );
3067
3742
  await MarkdownUtils.injectIndex(
3068
3743
  process.cwd(),
@@ -3121,10 +3796,12 @@ class SyncCommand {
3121
3796
  configService;
3122
3797
  detectionService;
3123
3798
  syncService;
3124
- constructor(configService, detectionService, syncService) {
3799
+ mcpService;
3800
+ constructor(configService, detectionService, syncService, mcpService) {
3125
3801
  this.configService = configService || new ConfigService();
3126
3802
  this.detectionService = detectionService || new DetectionService();
3127
3803
  this.syncService = syncService || new SyncService();
3804
+ this.mcpService = mcpService || new McpConfigService();
3128
3805
  }
3129
3806
  /**
3130
3807
  * Executes the synchronization flow.
@@ -3198,6 +3875,7 @@ class SyncCommand {
3198
3875
  await this.syncService.writeWorkflows(workflows, config);
3199
3876
  await this.syncService.applyIndices(config, config.agents);
3200
3877
  console.log(pc.green("✅ All skills synced successfully!"));
3878
+ await this.runMcpPhase(config, options);
3201
3879
  } catch (error) {
3202
3880
  if (error instanceof Error) {
3203
3881
  console.error(pc.red("❌ Sync failed:"), error.message);
@@ -3206,19 +3884,132 @@ class SyncCommand {
3206
3884
  }
3207
3885
  }
3208
3886
  }
3887
+ /**
3888
+ * Phase 7 — MCP integration. Three paths:
3889
+ * A. Never prompted yet: ask once now (with full value-prop), persist decision.
3890
+ * B. Already prompted, mcp.enabled=true: dispatch install at the configured scope.
3891
+ * C. Already prompted, mcp.enabled=false: skip silently.
3892
+ *
3893
+ * In non-interactive mode (no TTY) and never-prompted: skip with a hint, do
3894
+ * not assume consent. Users can opt in later via `ags mcp enable`.
3895
+ */
3896
+ async runMcpPhase(config, options) {
3897
+ const mcp = config.mcp ?? defaultMcpConfig();
3898
+ const agents = config.agents ?? [];
3899
+ if (!mcp.prompted) {
3900
+ if (!process.stdin.isTTY && !options.yes) {
3901
+ console.log(
3902
+ pc.gray(
3903
+ "\nℹ️ MCP integration not yet configured. Run `ags mcp enable` (or `ags mcp scope project`) when ready."
3904
+ )
3905
+ );
3906
+ return;
3907
+ }
3908
+ const decided = await this.promptMcpConsent(options);
3909
+ config.mcp = {
3910
+ enabled: decided.enabled,
3911
+ scope: decided.enabled ? decided.scope : "disabled",
3912
+ prompted: true
3913
+ };
3914
+ await this.configService.saveConfig(config);
3915
+ }
3916
+ const finalMcp = config.mcp ?? defaultMcpConfig();
3917
+ if (!finalMcp.enabled || finalMcp.scope === "disabled") {
3918
+ return;
3919
+ }
3920
+ const userScopePrompt = options.yes ? async () => true : async (agent, file) => {
3921
+ const { ok } = await inquirer.prompt([
3922
+ {
3923
+ type: "confirm",
3924
+ name: "ok",
3925
+ message: `Write user-scope MCP config for ${pc.cyan(agent)} at ${pc.gray(file)}?`,
3926
+ default: false
3927
+ }
3928
+ ]);
3929
+ return ok;
3930
+ };
3931
+ console.log(pc.cyan("\n🔌 Wiring MCP server..."));
3932
+ const report = await this.mcpService.install({
3933
+ rootDir: process.cwd(),
3934
+ agents,
3935
+ mcp: finalMcp,
3936
+ userScopePrompt
3937
+ });
3938
+ for (const w of report.projectWrites) {
3939
+ const tag = w.action === "added" ? pc.green(" + added ") : w.action === "updated" ? pc.cyan(" ~ updated ") : pc.gray(" = up-to-date");
3940
+ console.log(`${tag} ${w.agent.padEnd(12)} ${pc.gray(w.file)}`);
3941
+ }
3942
+ for (const w of report.userWrites) {
3943
+ console.log(` ${pc.green("+ wrote ")} ${w.agent.padEnd(12)} ${pc.gray(w.file)}`);
3944
+ }
3945
+ for (const d of report.declined) {
3946
+ console.log(` ${pc.gray("- ")} ${d.agent.padEnd(12)} ${pc.gray(d.file)} (declined)`);
3947
+ }
3948
+ if (report.snippets.length > 0) {
3949
+ console.log(
3950
+ pc.gray(` ${report.snippets.length} snippet(s) written to ./mcp-config-snippets/`)
3951
+ );
3952
+ }
3953
+ if (report.unsupported.length > 0) {
3954
+ console.log(
3955
+ pc.gray(` (skipped — no MCP support yet: ${report.unsupported.join(", ")})`)
3956
+ );
3957
+ }
3958
+ }
3959
+ async promptMcpConsent(options) {
3960
+ if (options.yes) {
3961
+ return { enabled: true, scope: "project" };
3962
+ }
3963
+ console.log(pc.bold("\n🔌 MCP server setup\n"));
3964
+ console.log("agent-skills-standard ships an optional MCP server that serves your skills");
3965
+ console.log("to AI agents at runtime. Quick comparison:\n");
3966
+ console.log(pc.bold(" WITHOUT MCP:") + " sub-agents skip skill loading; no audit trail");
3967
+ console.log(pc.bold(" WITH MCP: ") + " every sub-agent can call load_skills_for_files; auditable\n");
3968
+ const decision = await inquirer.prompt([
3969
+ {
3970
+ type: "confirm",
3971
+ name: "enabled",
3972
+ message: "Enable MCP server integration?",
3973
+ default: true
3974
+ },
3975
+ {
3976
+ type: "list",
3977
+ name: "scope",
3978
+ message: "Where should sync write MCP configs?",
3979
+ when: (a) => a.enabled === true,
3980
+ choices: [
3981
+ {
3982
+ name: "project (recommended) — only files inside this project",
3983
+ value: "project"
3984
+ },
3985
+ {
3986
+ name: "user — also $HOME files (sync prompts each)",
3987
+ value: "user"
3988
+ },
3989
+ {
3990
+ name: "snippets-only — never edits any runtime config",
3991
+ value: "snippets-only"
3992
+ }
3993
+ ],
3994
+ default: "project"
3995
+ }
3996
+ ]);
3997
+ return decision;
3998
+ }
3209
3999
  }
3210
- const version = "2.1.2";
4000
+ const version = "2.2.0";
3211
4001
  const pkg = {
3212
4002
  version
3213
4003
  };
3214
4004
  class UpgradeCommand {
4005
+ currentVersion = pkg.version;
3215
4006
  /**
3216
4007
  * Executes the upgrade flow.
3217
4008
  * @param options Command options, including `dryRun` to check for updates without installing.
3218
4009
  */
3219
4010
  async run(options) {
3220
4011
  console.log(pc.cyan("🔍 Checking for updates..."));
3221
- const currentVersion = pkg.version;
4012
+ const currentVersion = this.currentVersion;
3222
4013
  let latestVersion = null;
3223
4014
  try {
3224
4015
  latestVersion = child_process.execSync("npm view agent-skills-standard version", {
@@ -3737,7 +4528,7 @@ dotenv.config();
3737
4528
  const program = new commander.Command();
3738
4529
  program.name("agent-skills-standard").description(
3739
4530
  "A CLI to manage and sync AI agent skills for Cursor, Claude, Copilot, Windsurf, and more."
3740
- ).version("2.1.2");
4531
+ ).version("2.2.0");
3741
4532
  program.command("init").description("Initialize a .skillsrc configuration file interactively").action(async () => {
3742
4533
  const init = new InitCommand();
3743
4534
  await init.run();
@@ -3786,5 +4577,15 @@ program.command("upgrade").description("Upgrade the CLI to the latest version").
3786
4577
  const cmd = new UpgradeCommand();
3787
4578
  await cmd.run(options);
3788
4579
  });
4580
+ program.command("mcp <action> [scope]").description(
4581
+ "Manage the optional MCP server integration. Actions: status | enable | disable | scope <project|user|snippets-only|disabled> | install | uninstall | snippets"
4582
+ ).option("--scope <scope>", "Override scope for install (project|user|snippets-only|disabled)").option("--from <from>", "For uninstall: project | user | all (default: project)").action(
4583
+ async (action, positional, opts) => {
4584
+ const merged = { ...opts };
4585
+ if (positional) merged.scope = opts.scope ?? positional;
4586
+ const cmd = new McpCommand();
4587
+ await cmd.run(action, merged);
4588
+ }
4589
+ );
3789
4590
  program.parse();
3790
4591
  //# sourceMappingURL=index.js.map