Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ rulesync.local.jsonc
**/.rovodev/subagents/
**/.takt/facets/personas/
**/.vibe/agents/
**/.ai/mcp/mcp.json
**/.agents/mcp_config.json
**/.augment/settings.json
**/.mcp.json
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ The tables below show whether each tool supports a given feature (✅ = supporte
| Kiro IDE | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
| Google Antigravity IDE | ✅ | | ✅ | ✅ | | ✅ | ✅ | ✅ | |
| Google Antigravity CLI | ✅ | ✅ | ✅ | ✅ | | ✅ | ✅ | ✅ | |
| JetBrains AI Assistant | ✅ | ✅ | | | | ✅ | | | |
| JetBrains AI Assistant | ✅ | ✅ | | | | ✅ | | | |
| JetBrains Junie | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
| AugmentCode | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
| Devin Desktop | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
| Kiro IDE | kiro-ide | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ | |
| Google Antigravity IDE | antigravity-ide | ✅ 🌏 | | ✅ 🌏 🔧 | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | ✅ | |
| Google Antigravity CLI | antigravity-cli | ✅ 🌏 | ✅ | ✅ 🌏 🔧 | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | 🌏 | |
| JetBrains AI Assistant | aiassistant | ✅ | ✅ | | | | ✅ | | | |
| JetBrains AI Assistant | aiassistant | ✅ | ✅ | ✅ 🌏 | | | ✅ | | | |
| JetBrains Junie | junie | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | 🌏 | ✅ 🌏 | |
| AugmentCode | augmentcode | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | |
| Devin Desktop | devin | ✅ 🌏 | ✅ | ✅ 🌏 🔧 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | |
Expand Down
2 changes: 1 addition & 1 deletion skills/rulesync/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
| Kiro IDE | kiro-ide | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ | |
| Google Antigravity IDE | antigravity-ide | ✅ 🌏 | | ✅ 🌏 🔧 | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | ✅ | |
| Google Antigravity CLI | antigravity-cli | ✅ 🌏 | ✅ | ✅ 🌏 🔧 | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | 🌏 | |
| JetBrains AI Assistant | aiassistant | ✅ | ✅ | | | | ✅ | | | |
| JetBrains AI Assistant | aiassistant | ✅ | ✅ | ✅ 🌏 | | | ✅ | | | |
| JetBrains Junie | junie | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | 🌏 | ✅ 🌏 | |
| AugmentCode | augmentcode | ✅ 🌏 | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | |
| Devin Desktop | devin | ✅ 🌏 | ✅ | ✅ 🌏 🔧 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | |
Expand Down
6 changes: 6 additions & 0 deletions src/constants/aiassistant-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ export const AIASSISTANT_IGNORE_FILE_NAME = ".aiignore";
// directory, following the open Agent Skills standard. The relative path is the
// same as the agentsskills target. https://agentskills.io/specification
export const AIASSISTANT_SKILLS_DIR_PATH = AGENTSMD_SKILLS_DIR_PATH;
// JetBrains AI Assistant project-level MCP configuration is read from
// `.ai/mcp/mcp.json`. https://www.jetbrains.com/help/ai-assistant/mcp.html
export const AIASSISTANT_MCP_DIR = ".ai";
export const AIASSISTANT_MCP_SUB_DIR = "mcp";
export const AIASSISTANT_MCP_DIR_PATH = join(AIASSISTANT_MCP_DIR, AIASSISTANT_MCP_SUB_DIR);
export const AIASSISTANT_MCP_FILE_NAME = "mcp.json";
2 changes: 2 additions & 0 deletions src/e2e/e2e-mcp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {

// Native MCP tools that emit "test-server" (takt writes a transport allowlist instead).
const mcpGenerateTargets = [
{ target: "aiassistant", outputPath: join(".ai", "mcp", "mcp.json") },
{ target: "augmentcode", outputPath: join(".augment", "settings.json") },
{ target: "amp", outputPath: join(".amp", "settings.json") },
{ target: "claudecode", outputPath: ".mcp.json" },
Expand Down Expand Up @@ -606,6 +607,7 @@ describe("E2E: mcp (import)", () => {

// Native global-scope MCP tools that emit "test-server" (takt writes a transport allowlist instead).
const mcpGlobalTargets = [
{ target: "aiassistant", outputPath: join(".ai", "mcp", "mcp.json") },
{ target: "augmentcode", outputPath: join(".augment", "settings.json") },
{ target: "claudecode", outputPath: ".claude.json" },
{ target: "cursor", outputPath: join(".cursor", "mcp.json") },
Expand Down
122 changes: 122 additions & 0 deletions src/features/mcp/aiassistant-mcp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { join } from "node:path";

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import {
RULESYNC_MCP_SCHEMA_URL,
RULESYNC_RELATIVE_DIR_PATH,
} from "../../constants/rulesync-paths.js";
import { setupTestDirectory } from "../../test-utils/test-directories.js";
import { ensureDir, writeFileContent } from "../../utils/file.js";
import { AiassistantMcp } from "./aiassistant-mcp.js";
import { RulesyncMcp } from "./rulesync-mcp.js";

describe("AiassistantMcp", () => {
let testDir: string;
let cleanup: () => Promise<void>;

beforeEach(async () => {
({ testDir, cleanup } = await setupTestDirectory());
vi.spyOn(process, "cwd").mockReturnValue(testDir);
});

afterEach(async () => {
await cleanup();
vi.restoreAllMocks();
});

describe("constructor", () => {
it("creates an instance and parses JSON", () => {
const validJsonContent = JSON.stringify({
mcpServers: {
"test-server": { command: "node", args: ["server.js"] },
},
});

const aiassistantMcp = new AiassistantMcp({
relativeDirPath: ".ai/mcp",
relativeFilePath: "mcp.json",
fileContent: validJsonContent,
});

expect(aiassistantMcp).toBeInstanceOf(AiassistantMcp);
expect(aiassistantMcp.getRelativeDirPath()).toBe(".ai/mcp");
expect(aiassistantMcp.getRelativeFilePath()).toBe("mcp.json");
expect(aiassistantMcp.getJson()).toEqual(JSON.parse(validJsonContent));
});
});

describe("fromFile", () => {
it("reads .ai/mcp/mcp.json from disk", async () => {
const dir = join(testDir, ".ai/mcp");
await ensureDir(dir);
const filePath = join(dir, "mcp.json");
const content = JSON.stringify({ mcpServers: { A: { command: "echo" } } }, null, 2);
await writeFileContent(filePath, content);

const aiassistant = await AiassistantMcp.fromFile({ outputRoot: testDir, validate: true });

expect(aiassistant.getFilePath()).toBe(filePath);
expect(aiassistant.getFileContent()).toBe(content);
expect(aiassistant.getJson()).toEqual(JSON.parse(content));
});
});

describe("getSettablePaths", () => {
describe("getSettablePaths", () => {
it("returns the same .ai/mcp/mcp.json path for project and global mode", () => {
const projectPaths = AiassistantMcp.getSettablePaths({ global: false });
const globalPaths = AiassistantMcp.getSettablePaths({ global: true });
const expected = { relativeDirPath: join(".ai", "mcp"), relativeFilePath: "mcp.json" };
expect(projectPaths).toEqual(expected);
expect(globalPaths).toEqual(expected);
});
});
});

describe("fromRulesyncMcp", () => {
it("copies content from .rulesync/.mcp.json", async () => {
const rulesyncContent = JSON.stringify(
{ mcpServers: { B: { command: "node", args: ["b.js"] } } },
null,
2,
);
const rulesync = new RulesyncMcp({
outputRoot: testDir,
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
relativeFilePath: ".mcp.json",
fileContent: rulesyncContent,
});

const aiassistant = AiassistantMcp.fromRulesyncMcp({
outputRoot: testDir,
rulesyncMcp: rulesync,
});

expect(aiassistant.getRelativeDirPath()).toBe(".ai/mcp");
expect(aiassistant.getRelativeFilePath()).toBe("mcp.json");
expect(aiassistant.getFileContent()).toBe(rulesyncContent);
});
});

describe("toRulesyncMcp", () => {
it("maps back to a RulesyncMcp with same content", () => {
const content = JSON.stringify({ mcpServers: { X: { command: "echo" } } }, null, 2);
const aiassistant = new AiassistantMcp({
outputRoot: testDir,
relativeDirPath: ".ai/mcp",
relativeFilePath: "mcp.json",
fileContent: content,
});

const rulesync = aiassistant.toRulesyncMcp();
expect(rulesync).toBeInstanceOf(RulesyncMcp);
expect(rulesync.getRelativeDirPath()).toBe(RULESYNC_RELATIVE_DIR_PATH);
expect(rulesync.getRelativeFilePath()).toBe("mcp.json");
expect(JSON.parse(rulesync.getFileContent())).toEqual({
$schema: RULESYNC_MCP_SCHEMA_URL,
...JSON.parse(content),
});
});
});
});
113 changes: 113 additions & 0 deletions src/features/mcp/aiassistant-mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { join } from "node:path";

import {
AIASSISTANT_MCP_DIR_PATH,
AIASSISTANT_MCP_FILE_NAME,
} from "../../constants/aiassistant-paths.js";
import { ValidationResult } from "../../types/ai-file.js";
import { readFileContent } from "../../utils/file.js";
import { RulesyncMcp } from "./rulesync-mcp.js";
import {
ToolMcp,
ToolMcpForDeletionParams,
ToolMcpFromFileParams,
ToolMcpFromRulesyncMcpParams,
ToolMcpParams,
ToolMcpSettablePaths,
} from "./tool-mcp.js";

export class AiassistantMcp extends ToolMcp {
private readonly json: Record<string, unknown>;

constructor(params: ToolMcpParams) {
super(params);
this.json = this.fileContent !== undefined ? JSON.parse(this.fileContent) : {};
}

getJson(): Record<string, unknown> {
return this.json;
}

static getSettablePaths(_options: { global?: boolean } = {}): ToolMcpSettablePaths {
// JetBrains AI Assistant reads project-level MCP config from
// `.ai/mcp/mcp.json` and global (user-level) config from
// `~/.ai/mcp/mcp.json`. The relative path is identical for both scopes;
// the base directory changes (cwd for project, home dir for global).
return {
relativeDirPath: AIASSISTANT_MCP_DIR_PATH,
relativeFilePath: AIASSISTANT_MCP_FILE_NAME,
};
}

static async fromFile({
outputRoot = process.cwd(),
validate = true,
global = false,
}: ToolMcpFromFileParams): Promise<AiassistantMcp> {
const paths = this.getSettablePaths({ global });
const fileContent = await readFileContent(
join(outputRoot, paths.relativeDirPath, paths.relativeFilePath),
);

return new AiassistantMcp({
outputRoot,
relativeDirPath: paths.relativeDirPath,
relativeFilePath: paths.relativeFilePath,
fileContent,
validate,
global,
});
}

static fromRulesyncMcp({
outputRoot = process.cwd(),
rulesyncMcp,
validate = true,
global = false,
}: ToolMcpFromRulesyncMcpParams): AiassistantMcp {
const paths = this.getSettablePaths({ global });

// Preserve top-level fields ($schema, etc.) from the source JSON, but
// use getMcpServers() so rulesync-only fields and codex-only fields
// (`envVars`) are stripped before writing the aiassistant config.
const json = rulesyncMcp.getJson();
const fileContent = JSON.stringify(
{ ...json, mcpServers: rulesyncMcp.getMcpServers() },
null,
2,
);

return new AiassistantMcp({
outputRoot,
relativeDirPath: paths.relativeDirPath,
relativeFilePath: paths.relativeFilePath,
fileContent,
validate,
global,
});
}

toRulesyncMcp(): RulesyncMcp {
return this.toRulesyncMcpDefault();
}

validate(): ValidationResult {
return { success: true, error: null };
}

static forDeletion({
outputRoot = process.cwd(),
relativeDirPath,
relativeFilePath,
global = false,
}: ToolMcpForDeletionParams): AiassistantMcp {
return new AiassistantMcp({
outputRoot,
relativeDirPath,
relativeFilePath,
fileContent: "{}",
validate: false,
global,
});
}
}
18 changes: 18 additions & 0 deletions src/features/mcp/mcp-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { mcpProcessorToolTargetTuple } from "../../types/tool-target-tuples.js";
import { ToolTarget } from "../../types/tool-targets.js";
import { formatError } from "../../utils/error.js";
import type { Logger } from "../../utils/logger.js";
import { AiassistantMcp } from "./aiassistant-mcp.js";
import { AmpMcp } from "./amp-mcp.js";
import { AntigravityCliMcp } from "./antigravity-cli-mcp.js";
import { AntigravityIdeMcp } from "./antigravity-ide-mcp.js";
Expand Down Expand Up @@ -85,6 +86,23 @@ type ToolMcpFactory = {
* Using Map to preserve insertion order for consistent iteration.
*/
export const toolMcpFactories = new Map<McpProcessorToolTarget, ToolMcpFactory>([
[
"aiassistant",
{
// JetBrains AI Assistant reads project-level MCP config from
// `.ai/mcp/mcp.json` and global (user-level) config from
// `~/.ai/mcp/mcp.json`. The relative path is identical for both scopes;
// the base directory changes (cwd for project, home dir for global).
// https://www.jetbrains.com/help/ai-assistant/mcp.html
class: AiassistantMcp,
meta: {
supportsProject: true,
supportsGlobal: true,
supportsEnabledTools: false,
supportsDisabledTools: false,
},
},
],
[
"amp",
{
Expand Down
1 change: 1 addition & 0 deletions src/types/tool-target-tuples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export const ignoreProcessorToolTargetTuple = [
] as const;

export const mcpProcessorToolTargetTuple = [
"aiassistant",
"amp",
"antigravity-cli",
"antigravity-ide",
Expand Down
Loading