Skip to content

Commit 4f9afd5

Browse files
Merge pull request #394 from intellectronica/copilot/fix-junie-agent-mcp-support
Implement Junie MCP server support Fixes #393
2 parents 76760ba + c82cb72 commit 4f9afd5

File tree

11 files changed

+90
-3
lines changed

11 files changed

+90
-3
lines changed

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Ruler solves this by providing a **single source of truth** for all your AI agen
7373
| Firebase Studio | `.idx/airules.md` | `.idx/mcp.json` | - |
7474
| Open Hands | `.openhands/microagents/repo.md` | `config.toml` | - |
7575
| Gemini CLI | `AGENTS.md` | `.gemini/settings.json` | `.gemini/skills/` |
76-
| Junie | `.junie/guidelines.md` | - | - |
76+
| Junie | `.junie/guidelines.md` | `.junie/mcp/mcp.json` | - |
7777
| AugmentCode | `.augment/rules/ruler_augment_instructions.md` | - | - |
7878
| Kilo Code | `AGENTS.md` | `.kilocode/mcp.json` | `.claude/skills/` |
7979
| OpenCode | `AGENTS.md` | `opencode.json` | `.opencode/skills/` |
@@ -447,6 +447,10 @@ enabled = true
447447
enabled = true
448448
output_path = ".junie/guidelines.md"
449449

450+
[agents.junie.mcp]
451+
enabled = true
452+
merge_strategy = "merge"
453+
450454
# Agent-specific MCP configuration
451455
[agents.cursor.mcp]
452456
enabled = true

src/agents/JunieAgent.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,12 @@ export class JunieAgent extends AbstractAgent {
1616
getDefaultOutputPath(projectRoot: string): string {
1717
return path.join(projectRoot, '.junie', 'guidelines.md');
1818
}
19+
20+
supportsMcpStdio(): boolean {
21+
return true;
22+
}
23+
24+
supportsMcpRemote(): boolean {
25+
return true;
26+
}
1927
}

src/core/revert-engine.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ async function removeAdditionalAgentFiles(
313313
'.mcp.json',
314314
'.vscode/mcp.json',
315315
'.cursor/mcp.json',
316+
'.junie/mcp/mcp.json',
316317
'.kilocode/mcp.json',
317318
'config.toml',
318319
];

src/paths/mcp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export async function getNativeMcpPath(
3737
case 'Gemini CLI':
3838
candidates.push(path.join(projectRoot, '.gemini', 'settings.json'));
3939
break;
40+
case 'Junie':
41+
candidates.push(path.join(projectRoot, '.junie', 'mcp', 'mcp.json'));
42+
break;
4043
case 'Qwen Code':
4144
candidates.push(path.join(projectRoot, '.qwen', 'settings.json'));
4245
break;

tests/e2e/ruler.integration.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ File: extra-rules.md
251251

252252
// Junie
253253
'.junie/guidelines.md',
254+
'.junie/mcp/mcp.json',
254255

255256
// JetBrains AI Assistant
256257
'.aiassistant/rules/AGENTS.md',
@@ -302,6 +303,7 @@ File: extra-rules.md
302303
{ path: '.vscode/mcp.json', mcpKey: 'mcpServers' }, // Copilot
303304
{ path: '.kilocode/mcp.json', mcpKey: 'mcpServers' },
304305
{ path: '.mcp.json', mcpKey: 'mcpServers' }, // Aider
306+
{ path: '.junie/mcp/mcp.json', mcpKey: 'mcpServers' },
305307
];
306308

307309
for (const { path, expectedSchema, expectedKey, expectedValue, mcpKey } of jsonConfigFiles) {
@@ -440,6 +442,7 @@ File: extra-rules.md
440442
{ path: '.zed/settings.json', key: 'context_servers', name: 'Zed' },
441443
{ path: '.kilocode/mcp.json', key: 'mcpServers', name: 'Kilo Code' },
442444
{ path: '.mcp.json', key: 'mcpServers', name: 'Aider' },
445+
{ path: '.junie/mcp/mcp.json', key: 'mcpServers', name: 'Junie' },
443446
{ path: 'opencode.json', key: 'mcp', name: 'OpenCode' },
444447
];
445448

tests/e2e/ruler.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ describe('End-to-End Ruler CLI', () => {
6868
'config.toml',
6969
);
7070
const juniePath = path.join(projectRoot, '.junie', 'guidelines.md');
71+
const junieMcpPath = path.join(projectRoot, '.junie', 'mcp', 'mcp.json');
7172

7273
await Promise.all([
7374
expect(fs.readFile(claudePath, 'utf8')).resolves.toContain('Rule B'),
@@ -79,6 +80,7 @@ describe('End-to-End Ruler CLI', () => {
7980
fs.readFile(openHandsInstructionsPath, 'utf8'),
8081
).resolves.toContain('Rule A'),
8182
expect(fs.readFile(juniePath, 'utf8')).resolves.toContain('Rule B'),
83+
expect(fs.readFile(junieMcpPath, 'utf8')).resolves.toContain('"example"'),
8284
]);
8385
const ohToml = await fs.readFile(openHandsConfigPath, 'utf8');
8486
const ohParsed: any = parseTOML(ohToml);
@@ -195,6 +197,7 @@ output_path = "awesome.md"
195197
expect(gitignoreContent).toContain('.idx/airules.md');
196198
expect(gitignoreContent).toContain('.openhands/microagents/repo.md');
197199
expect(gitignoreContent).toContain('config.toml');
200+
expect(gitignoreContent).toContain('.junie/mcp/mcp.json');
198201
});
199202

200203
it('does not update .gitignore when --no-gitignore is used', async () => {
@@ -305,6 +308,7 @@ output_path = "custom-claude.md"`;
305308
'/.vscode/mcp.json',
306309
'/.gemini/settings.json',
307310
'/.cursor/mcp.json',
311+
'/.junie/mcp/mcp.json',
308312
'/.mcp.json',
309313
// Generated agent files (root-anchored)
310314
'/AGENTS.md',

tests/integration/revert-agents.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,15 @@ describe('Revert Agent Integration', () => {
9494
it('should handle MCP configuration files', async () => {
9595
await fs.writeFile(path.join(tmpDir, '.mcp.json'), '{"mcpServers": {}}');
9696
await fs.mkdir(path.join(tmpDir, '.vscode'), { recursive: true });
97+
await fs.mkdir(path.join(tmpDir, '.junie', 'mcp'), { recursive: true });
9798
await fs.writeFile(path.join(tmpDir, '.vscode', 'mcp.json'), '{"mcpServers": {}}');
99+
await fs.writeFile(path.join(tmpDir, '.junie', 'mcp', 'mcp.json'), '{"mcpServers": {}}');
98100

99101
await revertAllAgentConfigs(tmpDir, undefined, undefined, false, false, false);
100102

101103
await expect(fs.access(path.join(tmpDir, '.mcp.json'))).rejects.toThrow();
102104
await expect(fs.access(path.join(tmpDir, '.vscode', 'mcp.json'))).rejects.toThrow();
105+
await expect(fs.access(path.join(tmpDir, '.junie', 'mcp', 'mcp.json'))).rejects.toThrow();
103106
});
104107
});
105108

tests/unit/agents/JunieAgent.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ describe('JunieAgent', () => {
2323
expect(agent.getDefaultOutputPath('/root')).toBe('/root/.junie/guidelines.md');
2424
});
2525

26+
it('should support stdio MCP servers', () => {
27+
expect(agent.supportsMcpStdio()).toBe(true);
28+
});
29+
30+
it('should support remote MCP servers', () => {
31+
expect(agent.supportsMcpRemote()).toBe(true);
32+
});
33+
2634
it('should apply ruler config to the default output path', async () => {
2735
const ensureDirExists = jest.spyOn(FileSystemUtils, 'ensureDirExists');
2836
const backupFile = jest.spyOn(FileSystemUtils, 'backupFile');
@@ -46,4 +54,4 @@ describe('JunieAgent', () => {
4654
expect(backupFile).toHaveBeenCalledWith('/custom/path/guidelines.md');
4755
expect(writeGeneratedFile).toHaveBeenCalledWith('/custom/path/guidelines.md', 'rules');
4856
});
49-
});
57+
});

tests/unit/core/apply-engine.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import { IAgent } from '../../../src/agents/IAgent';
1515
import { ClaudeAgent } from '../../../src/agents/ClaudeAgent';
1616
import { CopilotAgent } from '../../../src/agents/CopilotAgent';
17+
import { JunieAgent } from '../../../src/agents/JunieAgent';
1718
import { LoadedConfig } from '../../../src/core/ConfigLoader';
1819
import * as FileSystemUtils from '../../../src/core/FileSystemUtils';
1920
import * as Constants from '../../../src/constants';
@@ -582,6 +583,51 @@ command = "sub-cmd"
582583

583584
logWarnSpy.mockRestore();
584585
});
586+
587+
it('writes Junie MCP config to the project-local .junie/mcp/mcp.json path', async () => {
588+
const config: LoadedConfig = {
589+
agentConfigs: {
590+
junie: {
591+
enabled: true,
592+
mcp: { enabled: true },
593+
},
594+
},
595+
};
596+
597+
const rules = '# Test rules';
598+
const mcpJson = {
599+
mcpServers: {
600+
context7: {
601+
command: 'npx',
602+
args: ['-y', '@upstash/context7-mcp'],
603+
},
604+
remote_api: {
605+
url: 'https://api.example.com/mcp',
606+
headers: {
607+
Authorization: 'Bearer test-token',
608+
},
609+
},
610+
},
611+
};
612+
613+
await applyConfigurationsToAgents(
614+
[new JunieAgent()],
615+
rules,
616+
mcpJson,
617+
config,
618+
tmpDir,
619+
false,
620+
false,
621+
true,
622+
undefined,
623+
false,
624+
);
625+
626+
const mcpPath = path.join(tmpDir, '.junie', 'mcp', 'mcp.json');
627+
const mcpContent = JSON.parse(await fs.readFile(mcpPath, 'utf8'));
628+
629+
expect(mcpContent).toEqual(mcpJson);
630+
});
585631
});
586632

587633
describe('updateGitignore', () => {

tests/unit/core/revert-engine.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,15 +195,20 @@ describe('revert-engine', () => {
195195
const geminiDir = path.join(tmpDir, '.gemini');
196196
const geminiSettings = path.join(geminiDir, 'settings.json');
197197
const mcpFile = path.join(tmpDir, '.mcp.json');
198+
const junieMcpDir = path.join(tmpDir, '.junie', 'mcp');
199+
const junieMcpFile = path.join(junieMcpDir, 'mcp.json');
198200

199201
await fs.mkdir(geminiDir, { recursive: true });
202+
await fs.mkdir(junieMcpDir, { recursive: true });
200203
await fs.writeFile(geminiSettings, '{}');
201204
await fs.writeFile(mcpFile, '{}');
205+
await fs.writeFile(junieMcpFile, '{}');
202206

203207
const result = await cleanUpAuxiliaryFiles(tmpDir, false, false);
204208

205209
expect(result.additionalFilesRemoved).toBeGreaterThan(0);
206210
expect(result.directoriesRemoved).toBeGreaterThan(0);
211+
await expect(fs.access(junieMcpFile)).rejects.toThrow();
207212
});
208213

209214
it('should handle dry run mode', async () => {
@@ -269,4 +274,4 @@ describe('revert-engine', () => {
269274
consoleErrorSpy.mockRestore();
270275
});
271276
});
272-
});
277+
});

0 commit comments

Comments
 (0)