@luquimbo/bi-superpowers 2.0.0 → 3.0.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.
Files changed (77) hide show
  1. package/.claude-plugin/marketplace.json +2 -24
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.claude-plugin/skill-manifest.json +2 -178
  4. package/.mcp.json +0 -16
  5. package/.plugin/plugin.json +1 -1
  6. package/AGENTS.md +37 -55
  7. package/CHANGELOG.md +69 -0
  8. package/README.md +74 -191
  9. package/bin/cli.js +45 -60
  10. package/bin/commands/install.js +37 -7
  11. package/bin/lib/generators/claude-plugin.js +6 -31
  12. package/bin/lib/generators/claude-plugin.test.js +12 -11
  13. package/bin/lib/generators/shared.js +0 -31
  14. package/bin/lib/mcp-config.js +242 -0
  15. package/bin/lib/mcp-config.test.js +184 -0
  16. package/bin/lib/microsoft-mcp.js +6 -20
  17. package/bin/lib/microsoft-mcp.test.js +25 -21
  18. package/bin/lib/skills.js +1 -2
  19. package/bin/postinstall.js +18 -23
  20. package/bin/utils/mcp-detect.js +4 -20
  21. package/bin/utils/mcp-detect.test.js +9 -33
  22. package/package.json +1 -1
  23. package/skills/pbi-connect/SKILL.md +1 -1
  24. package/skills/project-kickoff/SKILL.md +1 -1
  25. package/commands/contributions.md +0 -265
  26. package/commands/data-model-design.md +0 -468
  27. package/commands/dax-doctor.md +0 -248
  28. package/commands/fabric-scripts.md +0 -452
  29. package/commands/migration-assistant.md +0 -290
  30. package/commands/model-documenter.md +0 -242
  31. package/commands/report-layout.md +0 -296
  32. package/commands/rls-design.md +0 -533
  33. package/commands/theme-tweaker.md +0 -624
  34. package/skills/contributions/SKILL.md +0 -267
  35. package/skills/data-model-design/SKILL.md +0 -470
  36. package/skills/data-modeling/SKILL.md +0 -280
  37. package/skills/data-quality/SKILL.md +0 -664
  38. package/skills/dax/SKILL.md +0 -746
  39. package/skills/dax-doctor/SKILL.md +0 -250
  40. package/skills/dax-udf/SKILL.md +0 -489
  41. package/skills/deployment/SKILL.md +0 -320
  42. package/skills/excel-formulas/SKILL.md +0 -463
  43. package/skills/fabric-scripts/SKILL.md +0 -454
  44. package/skills/fast-standard/SKILL.md +0 -509
  45. package/skills/governance/SKILL.md +0 -258
  46. package/skills/migration-assistant/SKILL.md +0 -292
  47. package/skills/model-documenter/SKILL.md +0 -244
  48. package/skills/power-query/SKILL.md +0 -406
  49. package/skills/query-performance/SKILL.md +0 -480
  50. package/skills/report-design/SKILL.md +0 -207
  51. package/skills/report-layout/SKILL.md +0 -298
  52. package/skills/rls-design/SKILL.md +0 -535
  53. package/skills/semantic-model/SKILL.md +0 -237
  54. package/skills/testing-validation/SKILL.md +0 -643
  55. package/skills/theme-tweaker/SKILL.md +0 -626
  56. package/src/content/skills/contributions.md +0 -259
  57. package/src/content/skills/data-model-design.md +0 -462
  58. package/src/content/skills/data-modeling.md +0 -272
  59. package/src/content/skills/data-quality.md +0 -656
  60. package/src/content/skills/dax-doctor.md +0 -242
  61. package/src/content/skills/dax-udf.md +0 -481
  62. package/src/content/skills/dax.md +0 -738
  63. package/src/content/skills/deployment.md +0 -312
  64. package/src/content/skills/excel-formulas.md +0 -455
  65. package/src/content/skills/fabric-scripts.md +0 -446
  66. package/src/content/skills/fast-standard.md +0 -501
  67. package/src/content/skills/governance.md +0 -250
  68. package/src/content/skills/migration-assistant.md +0 -284
  69. package/src/content/skills/model-documenter.md +0 -236
  70. package/src/content/skills/power-query.md +0 -398
  71. package/src/content/skills/query-performance.md +0 -472
  72. package/src/content/skills/report-design.md +0 -199
  73. package/src/content/skills/report-layout.md +0 -290
  74. package/src/content/skills/rls-design.md +0 -527
  75. package/src/content/skills/semantic-model.md +0 -229
  76. package/src/content/skills/testing-validation.md +0 -635
  77. package/src/content/skills/theme-tweaker.md +0 -618
@@ -7,9 +7,6 @@
7
7
  * @module lib/generators/shared
8
8
  */
9
9
 
10
- const fs = require('fs');
11
- const path = require('path');
12
-
13
10
  /**
14
11
  * Extract skill metadata from markdown content
15
12
  *
@@ -243,33 +240,6 @@ ${codeStandards}
243
240
  ${footer}`;
244
241
  }
245
242
 
246
- /**
247
- * Create symlink to content cache directory for easy access to library
248
- * @param {string} targetDir - Project directory path
249
- * @param {string} contentCacheDir - Content cache directory path
250
- * @param {string} symlinkName - Name of the symlink to create
251
- */
252
- function createSymlink(targetDir, contentCacheDir, symlinkName) {
253
- const linkPath = path.join(targetDir, symlinkName);
254
- // Remove existing symlink if it exists
255
- if (fs.existsSync(linkPath)) {
256
- try {
257
- fs.unlinkSync(linkPath);
258
- } catch (e) {
259
- // Ignore errors
260
- }
261
- }
262
- try {
263
- fs.symlinkSync(contentCacheDir, linkPath, 'dir');
264
- console.log(` ✓ Created ${symlinkName} symlink`);
265
- } catch (e) {
266
- // Symlink may fail on Windows without admin rights
267
- if (process.env.DEBUG === 'true') {
268
- console.error(`[DEBUG] Failed to create symlink: ${e.message}`);
269
- }
270
- }
271
- }
272
-
273
243
  module.exports = {
274
244
  parseSkillMetadata,
275
245
  getSkillPurpose,
@@ -278,5 +248,4 @@ module.exports = {
278
248
  getFormatHeader,
279
249
  getFormatFooter,
280
250
  generateCombinedConfig,
281
- createSymlink,
282
251
  };
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Multi-Agent MCP Configuration Writer
3
+ * =====================================
4
+ *
5
+ * Writes the bi-superpowers MCP config (2 servers: powerbi-modeling-mcp
6
+ * + microsoft-learn) into each supported agent's expected config location
7
+ * at the user level (home directory).
8
+ *
9
+ * Each agent has its own config format and path. This module normalizes
10
+ * the write operation across all 5 supported agents:
11
+ *
12
+ * | Agent | Path | Format |
13
+ * |-----------------|---------------------------------------|-------------------|
14
+ * | Claude Code | ~/.claude.json | JSON mcpServers |
15
+ * | GitHub Copilot | ~/.copilot/mcp-config.json | JSON servers |
16
+ * | Codex | ~/.codex/config.toml | TOML mcp_servers |
17
+ * | Gemini CLI | ~/.gemini/settings.json | JSON mcpServers |
18
+ * | Kilo Code | ~/.kilocode/mcp_settings.json | JSON mcpServers |
19
+ *
20
+ * The launcher path for the Power BI Modeling MCP is resolved to an
21
+ * absolute path at install time so it works in agents that don't
22
+ * support Claude Code's `${CLAUDE_PLUGIN_ROOT}` variable.
23
+ *
24
+ * @module lib/mcp-config
25
+ */
26
+
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+ const os = require('os');
30
+
31
+ const MICROSOFT_LEARN_URL = 'https://learn.microsoft.com/api/mcp';
32
+ const MODELING_SERVER_NAME = 'powerbi-modeling';
33
+ const LEARN_SERVER_NAME = 'microsoft-learn';
34
+
35
+ /**
36
+ * Resolves the absolute path to the Power BI Modeling MCP launcher
37
+ * inside the installed npm package.
38
+ */
39
+ function getLauncherAbsolutePath(packageDir) {
40
+ return path.join(packageDir, 'bin', 'mcp', 'powerbi-modeling-launcher.js');
41
+ }
42
+
43
+ /**
44
+ * Safely read a JSON file. Returns null if missing or unparseable.
45
+ */
46
+ function readJsonSafe(filePath) {
47
+ if (!fs.existsSync(filePath)) return null;
48
+ try {
49
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
50
+ } catch (_) {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Write a JSON file, creating parent dirs if needed.
57
+ */
58
+ function writeJson(filePath, data) {
59
+ const dir = path.dirname(filePath);
60
+ if (!fs.existsSync(dir)) {
61
+ fs.mkdirSync(dir, { recursive: true });
62
+ }
63
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
64
+ }
65
+
66
+ /**
67
+ * Escape a string for use inside TOML double-quoted strings.
68
+ */
69
+ function tomlEscape(str) {
70
+ return String(str).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
71
+ }
72
+
73
+ // ============================================
74
+ // CLAUDE CODE
75
+ // ============================================
76
+ // Writes ~/.claude.json, adding to `mcpServers`. Preserves other user-level
77
+ // config in that file (projects, settings, etc.).
78
+ function writeClaudeCodeConfig(packageDir) {
79
+ const configPath = path.join(os.homedir(), '.claude.json');
80
+ const existing = readJsonSafe(configPath) || {};
81
+ const mcpServers = { ...(existing.mcpServers || {}) };
82
+
83
+ mcpServers[MODELING_SERVER_NAME] = {
84
+ command: 'node',
85
+ args: [getLauncherAbsolutePath(packageDir)],
86
+ };
87
+ mcpServers[LEARN_SERVER_NAME] = {
88
+ type: 'http',
89
+ url: MICROSOFT_LEARN_URL,
90
+ };
91
+
92
+ writeJson(configPath, { ...existing, mcpServers });
93
+ return configPath;
94
+ }
95
+
96
+ // ============================================
97
+ // GITHUB COPILOT CLI
98
+ // ============================================
99
+ // Writes ~/.copilot/mcp-config.json. Copilot uses `servers` (NOT mcpServers)
100
+ // and each server has an explicit `type` field.
101
+ function writeCopilotConfig(packageDir) {
102
+ const configPath = path.join(os.homedir(), '.copilot', 'mcp-config.json');
103
+ const existing = readJsonSafe(configPath) || {};
104
+ const servers = { ...(existing.servers || {}) };
105
+
106
+ servers[MODELING_SERVER_NAME] = {
107
+ type: 'stdio',
108
+ command: 'node',
109
+ args: [getLauncherAbsolutePath(packageDir)],
110
+ };
111
+ servers[LEARN_SERVER_NAME] = {
112
+ type: 'http',
113
+ url: MICROSOFT_LEARN_URL,
114
+ };
115
+
116
+ writeJson(configPath, { ...existing, servers });
117
+ return configPath;
118
+ }
119
+
120
+ // ============================================
121
+ // CODEX (OpenAI) — TOML format
122
+ // ============================================
123
+ // Writes ~/.codex/config.toml, appending [mcp_servers.*] sections.
124
+ // Preserves existing content by removing only our own sections before
125
+ // appending the fresh ones.
126
+ function writeCodexConfig(packageDir) {
127
+ const configPath = path.join(os.homedir(), '.codex', 'config.toml');
128
+ const launcher = getLauncherAbsolutePath(packageDir);
129
+
130
+ let existing = '';
131
+ if (fs.existsSync(configPath)) {
132
+ existing = fs.readFileSync(configPath, 'utf8');
133
+ }
134
+
135
+ // Remove any previous bi-superpowers MCP sections so re-running install
136
+ // doesn't duplicate them. Strips each section from its header to the
137
+ // next [ header or EOF.
138
+ const stripPattern = new RegExp(
139
+ `\\n?\\[mcp_servers\\.(${MODELING_SERVER_NAME}|${LEARN_SERVER_NAME})\\][\\s\\S]*?(?=\\n\\[|$)`,
140
+ 'g'
141
+ );
142
+ existing = existing.replace(stripPattern, '');
143
+
144
+ const newSections =
145
+ `\n\n[mcp_servers.${MODELING_SERVER_NAME}]\n` +
146
+ 'command = "node"\n' +
147
+ `args = ["${tomlEscape(launcher)}"]\n` +
148
+ `\n[mcp_servers.${LEARN_SERVER_NAME}]\n` +
149
+ `url = "${MICROSOFT_LEARN_URL}"\n`;
150
+
151
+ const content = existing.trimEnd() + newSections;
152
+
153
+ const dir = path.dirname(configPath);
154
+ if (!fs.existsSync(dir)) {
155
+ fs.mkdirSync(dir, { recursive: true });
156
+ }
157
+ fs.writeFileSync(configPath, content);
158
+ return configPath;
159
+ }
160
+
161
+ // ============================================
162
+ // GEMINI CLI
163
+ // ============================================
164
+ // Writes ~/.gemini/settings.json. Gemini uses `mcpServers` and `httpUrl`
165
+ // (not `url`) for HTTP transports.
166
+ function writeGeminiConfig(packageDir) {
167
+ const configPath = path.join(os.homedir(), '.gemini', 'settings.json');
168
+ const existing = readJsonSafe(configPath) || {};
169
+ const mcpServers = { ...(existing.mcpServers || {}) };
170
+
171
+ mcpServers[MODELING_SERVER_NAME] = {
172
+ command: 'node',
173
+ args: [getLauncherAbsolutePath(packageDir)],
174
+ };
175
+ mcpServers[LEARN_SERVER_NAME] = {
176
+ httpUrl: MICROSOFT_LEARN_URL,
177
+ };
178
+
179
+ writeJson(configPath, { ...existing, mcpServers });
180
+ return configPath;
181
+ }
182
+
183
+ // ============================================
184
+ // KILO CODE
185
+ // ============================================
186
+ // Writes ~/.kilocode/mcp_settings.json. Kilo uses `mcpServers` and `url`
187
+ // for HTTP transports.
188
+ function writeKiloConfig(packageDir) {
189
+ const configPath = path.join(os.homedir(), '.kilocode', 'mcp_settings.json');
190
+ const existing = readJsonSafe(configPath) || {};
191
+ const mcpServers = { ...(existing.mcpServers || {}) };
192
+
193
+ mcpServers[MODELING_SERVER_NAME] = {
194
+ command: 'node',
195
+ args: [getLauncherAbsolutePath(packageDir)],
196
+ };
197
+ mcpServers[LEARN_SERVER_NAME] = {
198
+ url: MICROSOFT_LEARN_URL,
199
+ };
200
+
201
+ writeJson(configPath, { ...existing, mcpServers });
202
+ return configPath;
203
+ }
204
+
205
+ /**
206
+ * Registry mapping agent IDs to their MCP config writers.
207
+ */
208
+ const MCP_WRITERS = {
209
+ 'claude-code': writeClaudeCodeConfig,
210
+ 'github-copilot': writeCopilotConfig,
211
+ codex: writeCodexConfig,
212
+ 'gemini-cli': writeGeminiConfig,
213
+ kilo: writeKiloConfig,
214
+ };
215
+
216
+ /**
217
+ * Write MCP config for a specific agent.
218
+ * @param {string} agentId - One of the supported agent IDs
219
+ * @param {string} packageDir - Absolute path to the installed package
220
+ * @returns {string|null} The config path written, or null if unknown agent
221
+ * @throws {Error} If the write fails
222
+ */
223
+ function writeMcpConfigForAgent(agentId, packageDir) {
224
+ const writer = MCP_WRITERS[agentId];
225
+ if (!writer) {
226
+ return null;
227
+ }
228
+ return writer(packageDir);
229
+ }
230
+
231
+ module.exports = {
232
+ writeMcpConfigForAgent,
233
+ MCP_WRITERS,
234
+ MODELING_SERVER_NAME,
235
+ LEARN_SERVER_NAME,
236
+ MICROSOFT_LEARN_URL,
237
+ // Exported for testing
238
+ readJsonSafe,
239
+ writeJson,
240
+ tomlEscape,
241
+ getLauncherAbsolutePath,
242
+ };
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Tests for the multi-agent MCP config writer.
3
+ */
4
+
5
+ const { test, describe, beforeEach, afterEach } = require('node:test');
6
+ const assert = require('node:assert');
7
+ const fs = require('node:fs');
8
+ const os = require('node:os');
9
+ const path = require('node:path');
10
+
11
+ const {
12
+ MCP_WRITERS,
13
+ MODELING_SERVER_NAME,
14
+ LEARN_SERVER_NAME,
15
+ MICROSOFT_LEARN_URL,
16
+ tomlEscape,
17
+ getLauncherAbsolutePath,
18
+ } = require('./mcp-config');
19
+
20
+ describe('mcp-config helpers', () => {
21
+ test('getLauncherAbsolutePath returns absolute launcher path', () => {
22
+ const pkgDir = '/tmp/fake-pkg';
23
+ const launcher = getLauncherAbsolutePath(pkgDir);
24
+ assert.strictEqual(launcher, path.join(pkgDir, 'bin', 'mcp', 'powerbi-modeling-launcher.js'));
25
+ });
26
+
27
+ test('tomlEscape escapes backslashes and quotes', () => {
28
+ assert.strictEqual(tomlEscape('plain'), 'plain');
29
+ assert.strictEqual(tomlEscape('C:\\path\\to\\file'), 'C:\\\\path\\\\to\\\\file');
30
+ assert.strictEqual(tomlEscape('say "hello"'), 'say \\"hello\\"');
31
+ });
32
+ });
33
+
34
+ describe('MCP_WRITERS registry', () => {
35
+ test('covers all 5 supported agents', () => {
36
+ const ids = Object.keys(MCP_WRITERS).sort();
37
+ assert.deepStrictEqual(ids, ['claude-code', 'codex', 'gemini-cli', 'github-copilot', 'kilo']);
38
+ });
39
+
40
+ test('each writer is a function', () => {
41
+ for (const [id, writer] of Object.entries(MCP_WRITERS)) {
42
+ assert.strictEqual(typeof writer, 'function', `${id} writer should be a function`);
43
+ }
44
+ });
45
+ });
46
+
47
+ describe('MCP writers — JSON agents', () => {
48
+ let tempHome;
49
+ let origHome;
50
+ const fakePkgDir = '/tmp/fake-bi-superpowers';
51
+
52
+ beforeEach(() => {
53
+ tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'bi-mcp-home-'));
54
+ origHome = os.homedir;
55
+ os.homedir = () => tempHome;
56
+ });
57
+
58
+ afterEach(() => {
59
+ os.homedir = origHome;
60
+ fs.rmSync(tempHome, { recursive: true, force: true });
61
+ });
62
+
63
+ test('claude-code writer produces mcpServers JSON at ~/.claude.json', () => {
64
+ const configPath = MCP_WRITERS['claude-code'](fakePkgDir);
65
+
66
+ assert.strictEqual(configPath, path.join(tempHome, '.claude.json'));
67
+ assert.ok(fs.existsSync(configPath));
68
+
69
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
70
+ assert.ok(config.mcpServers);
71
+ assert.ok(config.mcpServers[MODELING_SERVER_NAME]);
72
+ assert.strictEqual(config.mcpServers[MODELING_SERVER_NAME].command, 'node');
73
+ assert.ok(
74
+ config.mcpServers[MODELING_SERVER_NAME].args[0].includes('powerbi-modeling-launcher.js')
75
+ );
76
+ assert.deepStrictEqual(config.mcpServers[LEARN_SERVER_NAME], {
77
+ type: 'http',
78
+ url: MICROSOFT_LEARN_URL,
79
+ });
80
+ });
81
+
82
+ test('github-copilot writer uses "servers" key (not mcpServers)', () => {
83
+ const configPath = MCP_WRITERS['github-copilot'](fakePkgDir);
84
+
85
+ assert.strictEqual(configPath, path.join(tempHome, '.copilot', 'mcp-config.json'));
86
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
87
+ assert.ok(config.servers);
88
+ assert.strictEqual(config.mcpServers, undefined);
89
+ assert.strictEqual(config.servers[MODELING_SERVER_NAME].type, 'stdio');
90
+ assert.strictEqual(config.servers[LEARN_SERVER_NAME].type, 'http');
91
+ });
92
+
93
+ test('gemini-cli writer uses httpUrl (not url)', () => {
94
+ const configPath = MCP_WRITERS['gemini-cli'](fakePkgDir);
95
+
96
+ assert.strictEqual(configPath, path.join(tempHome, '.gemini', 'settings.json'));
97
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
98
+ assert.ok(config.mcpServers);
99
+ assert.strictEqual(config.mcpServers[LEARN_SERVER_NAME].httpUrl, MICROSOFT_LEARN_URL);
100
+ assert.strictEqual(config.mcpServers[LEARN_SERVER_NAME].url, undefined);
101
+ });
102
+
103
+ test('kilo writer uses url (standard)', () => {
104
+ const configPath = MCP_WRITERS.kilo(fakePkgDir);
105
+
106
+ assert.strictEqual(configPath, path.join(tempHome, '.kilocode', 'mcp_settings.json'));
107
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
108
+ assert.ok(config.mcpServers);
109
+ assert.strictEqual(config.mcpServers[LEARN_SERVER_NAME].url, MICROSOFT_LEARN_URL);
110
+ });
111
+
112
+ test('claude-code writer preserves existing config', () => {
113
+ const configPath = path.join(tempHome, '.claude.json');
114
+ fs.writeFileSync(
115
+ configPath,
116
+ JSON.stringify({
117
+ otherSetting: 'value',
118
+ mcpServers: { 'existing-mcp': { command: 'echo' } },
119
+ })
120
+ );
121
+
122
+ MCP_WRITERS['claude-code'](fakePkgDir);
123
+
124
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
125
+ assert.strictEqual(config.otherSetting, 'value');
126
+ assert.ok(config.mcpServers['existing-mcp']);
127
+ assert.ok(config.mcpServers[MODELING_SERVER_NAME]);
128
+ assert.ok(config.mcpServers[LEARN_SERVER_NAME]);
129
+ });
130
+ });
131
+
132
+ describe('codex writer — TOML', () => {
133
+ let tempHome;
134
+ let origHome;
135
+
136
+ beforeEach(() => {
137
+ tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'bi-mcp-codex-'));
138
+ origHome = os.homedir;
139
+ os.homedir = () => tempHome;
140
+ });
141
+
142
+ afterEach(() => {
143
+ os.homedir = origHome;
144
+ fs.rmSync(tempHome, { recursive: true, force: true });
145
+ });
146
+
147
+ test('writes TOML with [mcp_servers.*] sections', () => {
148
+ const configPath = MCP_WRITERS.codex('/tmp/fake-pkg');
149
+
150
+ assert.strictEqual(configPath, path.join(tempHome, '.codex', 'config.toml'));
151
+ const content = fs.readFileSync(configPath, 'utf8');
152
+
153
+ assert.ok(content.includes('[mcp_servers.powerbi-modeling]'));
154
+ assert.ok(content.includes('command = "node"'));
155
+ assert.ok(content.includes('[mcp_servers.microsoft-learn]'));
156
+ assert.ok(content.includes(`url = "${MICROSOFT_LEARN_URL}"`));
157
+ });
158
+
159
+ test('preserves existing non-MCP TOML content', () => {
160
+ const configPath = path.join(tempHome, '.codex', 'config.toml');
161
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
162
+ fs.writeFileSync(configPath, '[profile]\nname = "default"\n');
163
+
164
+ MCP_WRITERS.codex('/tmp/fake-pkg');
165
+
166
+ const content = fs.readFileSync(configPath, 'utf8');
167
+ assert.ok(content.includes('[profile]'));
168
+ assert.ok(content.includes('name = "default"'));
169
+ assert.ok(content.includes('[mcp_servers.powerbi-modeling]'));
170
+ });
171
+
172
+ test('re-running install replaces MCP sections, does not duplicate', () => {
173
+ MCP_WRITERS.codex('/tmp/fake-pkg');
174
+ MCP_WRITERS.codex('/tmp/fake-pkg');
175
+
176
+ const content = fs.readFileSync(path.join(tempHome, '.codex', 'config.toml'), 'utf8');
177
+
178
+ // Each section name should appear exactly once
179
+ const modelingCount = (content.match(/\[mcp_servers\.powerbi-modeling\]/g) || []).length;
180
+ const learnCount = (content.match(/\[mcp_servers\.microsoft-learn\]/g) || []).length;
181
+ assert.strictEqual(modelingCount, 1);
182
+ assert.strictEqual(learnCount, 1);
183
+ });
184
+ });
@@ -2,21 +2,20 @@
2
2
  * Official Microsoft MCP Configuration Helpers
3
3
  * ============================================
4
4
  *
5
- * Single source of truth for the Claude Code plugin and legacy adapter
6
- * configurations that point at the official Microsoft Power BI / Fabric
7
- * MCP servers.
5
+ * Single source of truth for the MCP servers that bi-superpowers ships.
6
+ * Currently ships 2 servers, both from Microsoft:
7
+ *
8
+ * - powerbi-modeling-mcp: local stdio launcher that talks to Power BI Desktop
9
+ * via XMLA on localhost. Requires Power BI Desktop running with a model open.
10
+ * - microsoft-learn: HTTP MCP that pipes Microsoft Learn docs into the agent.
8
11
  *
9
12
  * @module lib/microsoft-mcp
10
13
  */
11
14
 
12
15
  const path = require('path');
13
16
 
14
- const REMOTE_POWERBI_URL = 'https://api.fabric.microsoft.com/v1/mcp/powerbi';
15
- const FABRIC_MCP_PACKAGE = '@microsoft/fabric-mcp@latest';
16
17
  const MICROSOFT_LEARN_URL = 'https://learn.microsoft.com/api/mcp';
17
18
  const MODELING_SERVER_NAME = 'powerbi-modeling-mcp';
18
- const REMOTE_SERVER_NAME = 'powerbi-remote';
19
- const FABRIC_SERVER_NAME = 'fabric-mcp-server';
20
19
  const LEARN_SERVER_NAME = 'microsoft-learn';
21
20
  const ABSOLUTE_LAUNCHER_MODE = 'absolute';
22
21
  const PLUGIN_ROOT_LAUNCHER_MODE = 'plugin-root';
@@ -51,15 +50,6 @@ function createPluginMcpConfig(options = {}) {
51
50
  const launcherPath = resolveModelingLauncherPath(options);
52
51
 
53
52
  return {
54
- [REMOTE_SERVER_NAME]: {
55
- type: 'http',
56
- url: REMOTE_POWERBI_URL,
57
- },
58
- [FABRIC_SERVER_NAME]: {
59
- type: 'stdio',
60
- command: 'npx',
61
- args: ['-y', FABRIC_MCP_PACKAGE, 'server', 'start', '--mode', 'all'],
62
- },
63
53
  [MODELING_SERVER_NAME]: {
64
54
  type: 'stdio',
65
55
  command: 'node',
@@ -170,12 +160,8 @@ function mergeMcpConfig(existingConfig, newConfig, format) {
170
160
  module.exports = {
171
161
  ABSOLUTE_LAUNCHER_MODE,
172
162
  PLUGIN_ROOT_LAUNCHER_MODE,
173
- REMOTE_POWERBI_URL,
174
- FABRIC_MCP_PACKAGE,
175
163
  MICROSOFT_LEARN_URL,
176
164
  MODELING_SERVER_NAME,
177
- REMOTE_SERVER_NAME,
178
- FABRIC_SERVER_NAME,
179
165
  LEARN_SERVER_NAME,
180
166
  resolveModelingLauncherPath,
181
167
  createPluginMcpConfig,
@@ -9,8 +9,6 @@ const path = require('node:path');
9
9
  const {
10
10
  ABSOLUTE_LAUNCHER_MODE,
11
11
  PLUGIN_ROOT_LAUNCHER_MODE,
12
- REMOTE_POWERBI_URL,
13
- FABRIC_MCP_PACKAGE,
14
12
  MICROSOFT_LEARN_URL,
15
13
  createPluginMcpConfig,
16
14
  createMcpConfigForFormat,
@@ -41,29 +39,36 @@ describe('resolveModelingLauncherPath', () => {
41
39
  });
42
40
 
43
41
  describe('createPluginMcpConfig', () => {
44
- test('returns the official Microsoft MCP defaults', () => {
42
+ test('returns the 2 official Microsoft MCP defaults', () => {
45
43
  const config = createPluginMcpConfig({
46
44
  packageDir: '/tmp/bi-superpowers',
47
45
  launcherMode: ABSOLUTE_LAUNCHER_MODE,
48
46
  });
49
47
 
50
- assert.deepStrictEqual(config['powerbi-remote'], {
51
- type: 'http',
52
- url: REMOTE_POWERBI_URL,
53
- });
54
- assert.deepStrictEqual(config['fabric-mcp-server'], {
55
- type: 'stdio',
56
- command: 'npx',
57
- args: ['-y', FABRIC_MCP_PACKAGE, 'server', 'start', '--mode', 'all'],
58
- });
48
+ // Should have exactly 2 servers
49
+ assert.strictEqual(Object.keys(config).length, 2);
50
+
51
+ // powerbi-modeling-mcp: local stdio launcher
59
52
  assert.strictEqual(config['powerbi-modeling-mcp'].type, 'stdio');
60
53
  assert.strictEqual(config['powerbi-modeling-mcp'].command, 'node');
61
54
  assert.ok(config['powerbi-modeling-mcp'].args[0].endsWith('powerbi-modeling-launcher.js'));
55
+
56
+ // microsoft-learn: HTTP MCP
62
57
  assert.deepStrictEqual(config['microsoft-learn'], {
63
58
  type: 'http',
64
59
  url: MICROSOFT_LEARN_URL,
65
60
  });
66
61
  });
62
+
63
+ test('does not include the old removed servers', () => {
64
+ const config = createPluginMcpConfig({
65
+ packageDir: '/tmp/bi-superpowers',
66
+ launcherMode: ABSOLUTE_LAUNCHER_MODE,
67
+ });
68
+
69
+ assert.strictEqual(config['powerbi-remote'], undefined);
70
+ assert.strictEqual(config['fabric-mcp-server'], undefined);
71
+ });
67
72
  });
68
73
 
69
74
  describe('createMcpConfigForFormat', () => {
@@ -71,10 +76,9 @@ describe('createMcpConfigForFormat', () => {
71
76
  const pluginConfig = createMcpConfigForFormat('plugin');
72
77
  const cursorConfig = createMcpConfigForFormat('cursor');
73
78
 
74
- assert.ok(pluginConfig['powerbi-remote']);
75
- assert.ok(cursorConfig['powerbi-remote']);
76
- assert.strictEqual(pluginConfig['powerbi-remote'].type, 'http');
77
- assert.strictEqual(cursorConfig['powerbi-remote'].type, 'http');
79
+ assert.ok(pluginConfig['powerbi-modeling-mcp']);
80
+ assert.ok(cursorConfig['microsoft-learn']);
81
+ assert.strictEqual(pluginConfig['microsoft-learn'].type, 'http');
78
82
  });
79
83
 
80
84
  test('returns nested mcpServers config for claude-like outputs', () => {
@@ -82,7 +86,7 @@ describe('createMcpConfigForFormat', () => {
82
86
 
83
87
  assert.ok(config.mcpServers);
84
88
  assert.ok(config.mcpServers['powerbi-modeling-mcp']);
85
- assert.ok(config.mcpServers['fabric-mcp-server']);
89
+ assert.ok(config.mcpServers['microsoft-learn']);
86
90
  });
87
91
  });
88
92
 
@@ -90,22 +94,22 @@ describe('mergeMcpConfig', () => {
90
94
  test('merges plugin configs without dropping existing entries', () => {
91
95
  const merged = mergeMcpConfig(
92
96
  { existing: { type: 'http', url: 'https://example.com' } },
93
- { 'powerbi-remote': { type: 'http', url: REMOTE_POWERBI_URL } },
97
+ { 'microsoft-learn': { type: 'http', url: MICROSOFT_LEARN_URL } },
94
98
  'plugin'
95
99
  );
96
100
 
97
101
  assert.ok(merged.existing);
98
- assert.ok(merged['powerbi-remote']);
102
+ assert.ok(merged['microsoft-learn']);
99
103
  });
100
104
 
101
105
  test('merges claude configs inside mcpServers', () => {
102
106
  const merged = mergeMcpConfig(
103
107
  { mcpServers: { existing: { type: 'http', url: 'https://example.com' } } },
104
- { mcpServers: { 'powerbi-remote': { type: 'http', url: REMOTE_POWERBI_URL } } },
108
+ { mcpServers: { 'microsoft-learn': { type: 'http', url: MICROSOFT_LEARN_URL } } },
105
109
  'claude'
106
110
  );
107
111
 
108
112
  assert.ok(merged.mcpServers.existing);
109
- assert.ok(merged.mcpServers['powerbi-remote']);
113
+ assert.ok(merged.mcpServers['microsoft-learn']);
110
114
  });
111
115
  });
package/bin/lib/skills.js CHANGED
@@ -3,8 +3,7 @@
3
3
  * =======================
4
4
  *
5
5
  * Centralizes how BI Agent Superpowers loads skill source files from
6
- * the installed npm package. Since the project is open source, there's
7
- * no premium content cache — everything ships with the package.
6
+ * the installed npm package. Everything ships with the package.
8
7
  *
9
8
  * @module lib/skills
10
9
  */