@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.
- package/.claude-plugin/marketplace.json +2 -24
- package/.claude-plugin/plugin.json +1 -1
- package/.claude-plugin/skill-manifest.json +2 -178
- package/.mcp.json +0 -16
- package/.plugin/plugin.json +1 -1
- package/AGENTS.md +37 -55
- package/CHANGELOG.md +69 -0
- package/README.md +74 -191
- package/bin/cli.js +45 -60
- package/bin/commands/install.js +37 -7
- package/bin/lib/generators/claude-plugin.js +6 -31
- package/bin/lib/generators/claude-plugin.test.js +12 -11
- package/bin/lib/generators/shared.js +0 -31
- package/bin/lib/mcp-config.js +242 -0
- package/bin/lib/mcp-config.test.js +184 -0
- package/bin/lib/microsoft-mcp.js +6 -20
- package/bin/lib/microsoft-mcp.test.js +25 -21
- package/bin/lib/skills.js +1 -2
- package/bin/postinstall.js +18 -23
- package/bin/utils/mcp-detect.js +4 -20
- package/bin/utils/mcp-detect.test.js +9 -33
- package/package.json +1 -1
- package/skills/pbi-connect/SKILL.md +1 -1
- package/skills/project-kickoff/SKILL.md +1 -1
- package/commands/contributions.md +0 -265
- package/commands/data-model-design.md +0 -468
- package/commands/dax-doctor.md +0 -248
- package/commands/fabric-scripts.md +0 -452
- package/commands/migration-assistant.md +0 -290
- package/commands/model-documenter.md +0 -242
- package/commands/report-layout.md +0 -296
- package/commands/rls-design.md +0 -533
- package/commands/theme-tweaker.md +0 -624
- package/skills/contributions/SKILL.md +0 -267
- package/skills/data-model-design/SKILL.md +0 -470
- package/skills/data-modeling/SKILL.md +0 -280
- package/skills/data-quality/SKILL.md +0 -664
- package/skills/dax/SKILL.md +0 -746
- package/skills/dax-doctor/SKILL.md +0 -250
- package/skills/dax-udf/SKILL.md +0 -489
- package/skills/deployment/SKILL.md +0 -320
- package/skills/excel-formulas/SKILL.md +0 -463
- package/skills/fabric-scripts/SKILL.md +0 -454
- package/skills/fast-standard/SKILL.md +0 -509
- package/skills/governance/SKILL.md +0 -258
- package/skills/migration-assistant/SKILL.md +0 -292
- package/skills/model-documenter/SKILL.md +0 -244
- package/skills/power-query/SKILL.md +0 -406
- package/skills/query-performance/SKILL.md +0 -480
- package/skills/report-design/SKILL.md +0 -207
- package/skills/report-layout/SKILL.md +0 -298
- package/skills/rls-design/SKILL.md +0 -535
- package/skills/semantic-model/SKILL.md +0 -237
- package/skills/testing-validation/SKILL.md +0 -643
- package/skills/theme-tweaker/SKILL.md +0 -626
- package/src/content/skills/contributions.md +0 -259
- package/src/content/skills/data-model-design.md +0 -462
- package/src/content/skills/data-modeling.md +0 -272
- package/src/content/skills/data-quality.md +0 -656
- package/src/content/skills/dax-doctor.md +0 -242
- package/src/content/skills/dax-udf.md +0 -481
- package/src/content/skills/dax.md +0 -738
- package/src/content/skills/deployment.md +0 -312
- package/src/content/skills/excel-formulas.md +0 -455
- package/src/content/skills/fabric-scripts.md +0 -446
- package/src/content/skills/fast-standard.md +0 -501
- package/src/content/skills/governance.md +0 -250
- package/src/content/skills/migration-assistant.md +0 -284
- package/src/content/skills/model-documenter.md +0 -236
- package/src/content/skills/power-query.md +0 -398
- package/src/content/skills/query-performance.md +0 -472
- package/src/content/skills/report-design.md +0 -199
- package/src/content/skills/report-layout.md +0 -290
- package/src/content/skills/rls-design.md +0 -527
- package/src/content/skills/semantic-model.md +0 -229
- package/src/content/skills/testing-validation.md +0 -635
- 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
|
+
});
|
package/bin/lib/microsoft-mcp.js
CHANGED
|
@@ -2,21 +2,20 @@
|
|
|
2
2
|
* Official Microsoft MCP Configuration Helpers
|
|
3
3
|
* ============================================
|
|
4
4
|
*
|
|
5
|
-
* Single source of truth for the
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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-
|
|
75
|
-
assert.ok(cursorConfig['
|
|
76
|
-
assert.strictEqual(pluginConfig['
|
|
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['
|
|
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
|
-
{ '
|
|
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['
|
|
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: { '
|
|
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['
|
|
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.
|
|
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
|
*/
|