Skip to content

Commit c76faf2

Browse files
committed
feat: enhance MCP server handling and streamline configuration management
1 parent 1c782cd commit c76faf2

9 files changed

Lines changed: 173 additions & 222 deletions

File tree

.github/workflows/release.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ jobs:
5353

5454
env:
5555
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
56+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
5657
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
5758
NPM_CONFIG_PROVENANCE: true
5859
HUSKY: 0

cli/src/adapters/claude-code.ts

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { join } from "node:path";
88
import type { MCPServer } from "@src/types/models.js";
99
import * as fileOps from "@src/utils/file-ops.js";
1010
import { hashMCPServer } from "@src/utils/hash.js";
11+
import { inferMCPType, populateCommonMCPFields } from "@src/utils/mcp-utils.js";
1112
import type { WriteResult } from "./base.js";
1213
import { BaseAdapter } from "./base.js";
1314

@@ -66,20 +67,8 @@ export class ClaudeCodeAdapter extends BaseAdapter {
6667
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
6768
const rawConfig = serverConfig as Record<string, unknown>;
6869

69-
// Infer server type from configuration fields
70-
const hasCommand = typeof rawConfig.command === "string";
71-
const hasUrl = typeof rawConfig.url === "string";
72-
const hasAuth =
73-
rawConfig.auth !== undefined && typeof rawConfig.auth === "object";
74-
75-
let type: MCPServer["type"] = "stdio";
76-
if (hasCommand) {
77-
type = "stdio";
78-
} else if (hasAuth) {
79-
type = "oauth";
80-
} else if (hasUrl) {
81-
type = "http";
82-
}
70+
// DRY: Use shared type inference
71+
const type = inferMCPType(rawConfig);
8372

8473
// Create MCP server object (omit undefined optional fields)
8574
const server: MCPServer = {
@@ -88,23 +77,11 @@ export class ClaudeCodeAdapter extends BaseAdapter {
8877
hash: "", // Will be computed
8978
};
9079

91-
// Add optional fields only if they have values
92-
if (rawConfig.command) {
93-
server.command = rawConfig.command as string;
94-
}
95-
if (rawConfig.args) {
96-
server.args = rawConfig.args as string[];
97-
}
98-
if (rawConfig.env) {
99-
server.env = rawConfig.env as Record<string, string>;
100-
}
101-
if (rawConfig.url) {
102-
server.url = rawConfig.url as string;
103-
}
104-
if (rawConfig.headers) {
105-
server.headers = rawConfig.headers as Record<string, string>;
106-
}
107-
if (hasAuth) {
80+
// DRY: Use shared field population
81+
populateCommonMCPFields(server, rawConfig);
82+
83+
// Handle OAuth-specific auth field
84+
if (rawConfig.auth && typeof rawConfig.auth === "object") {
10885
const rawAuth = rawConfig.auth as Record<string, unknown>;
10986
const auth: MCPServer["auth"] = {
11087
client_id: (rawAuth.client_id as string) || "",

cli/src/adapters/cursor.ts

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { MCPServer, MCPOAuth } from "@src/types/models.js";
1010
import { EnvVarTransformer } from "@src/utils/env-var-transformer.js";
1111
import * as fileOps from "@src/utils/file-ops.js";
1212
import { hashMCPServer } from "@src/utils/hash.js";
13+
import { inferMCPType, populateCommonMCPFields } from "@src/utils/mcp-utils.js";
1314
import type { WriteResult } from "./base.js";
1415
import { BaseAdapter } from "./base.js";
1516

@@ -56,32 +57,19 @@ export class CursorAdapter extends BaseAdapter {
5657
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
5758
const rawConfig = serverConfig as Record<string, unknown>;
5859

59-
const hasCommand = typeof rawConfig.command === "string";
60-
const hasAuth =
61-
rawConfig.auth !== undefined && typeof rawConfig.auth === "object";
62-
const hasUrl = typeof rawConfig.url === "string";
63-
64-
let type: MCPServer["type"] = "stdio";
65-
if (hasCommand) {
66-
type = "stdio";
67-
} else if (hasAuth) {
68-
type = "oauth";
69-
} else if (hasUrl) {
70-
type = "http";
71-
}
60+
// DRY: Use shared type inference
61+
const type = inferMCPType(rawConfig);
7262

7363
const server: MCPServer = {
7464
name,
7565
type,
7666
hash: "",
7767
};
7868

79-
if (rawConfig.command) server.command = rawConfig.command as string;
80-
if (rawConfig.args) server.args = rawConfig.args as string[];
81-
if (rawConfig.env) server.env = rawConfig.env as Record<string, string>;
82-
if (rawConfig.url) server.url = rawConfig.url as string;
83-
if (rawConfig.headers)
84-
server.headers = rawConfig.headers as Record<string, string>;
69+
// DRY: Use shared field population
70+
populateCommonMCPFields(server, rawConfig);
71+
72+
// Handle Cursor-specific auth format
8573
if (rawConfig.auth && typeof rawConfig.auth === "object") {
8674
const normalized = this.fromCursorAuth(
8775
rawConfig.auth as Record<string, unknown>,

cli/src/commands/init.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { Command } from "commander";
1111
import inquirer from "inquirer";
1212
import ora from "ora";
1313
import { getAllConfigDirs, getToolChoices } from "@src/adapters/registry.js";
14+
import { saveConfig as saveConfigToFile } from "@src/core/config-manager.js";
1415
import type { ToolName, VibeConfig, ConfigLevel } from "@src/types/config.js";
15-
import { atomicWrite } from "@src/utils/atomic-write.js";
1616
import { t } from "@src/utils/i18n.js";
1717

1818
/**
@@ -92,18 +92,20 @@ export async function generateConfig(
9292

9393
/**
9494
* Save config to disk
95+
* DRY: Delegates to config-manager's saveConfig to avoid duplication
9596
*
9697
* @param config - Config to save
97-
* @param projectDir - Project directory
98+
* @param dir - Directory to save config in (project dir or user home dir based on config.level)
9899
*/
99100
export async function saveConfig(
100101
config: VibeConfig,
101-
projectDir: string,
102+
dir: string,
102103
): Promise<void> {
103-
const configPath = join(projectDir, ".vibe-sync.json");
104-
const content = JSON.stringify(config, null, 2);
105-
106-
await atomicWrite(configPath, content);
104+
// Pass dir as both projectDir and userDir since this function doesn't
105+
// distinguish between them - the caller is expected to pass the correct dir
106+
const projectDir = config.level === "project" ? dir : undefined;
107+
const userDir = config.level === "user" ? dir : undefined;
108+
await saveConfigToFile(config, config.level, projectDir, userDir);
107109
}
108110

109111
/**

cli/src/core/planner.ts

Lines changed: 16 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -235,70 +235,32 @@ export function formatPlan(plan: SyncPlan): string {
235235
processOperations(diff.toDelete, "DELETE", toolName);
236236
}
237237

238-
// Format grouped operations
239-
const createOps = Array.from(itemOperations.values()).filter(
240-
(op) => op.operation === "CREATE",
241-
);
242-
const updateOps = Array.from(itemOperations.values()).filter(
243-
(op) => op.operation === "UPDATE",
244-
);
245-
const deleteOps = Array.from(itemOperations.values()).filter(
246-
(op) => op.operation === "DELETE",
247-
);
248-
249-
if (createOps.length > 0) {
250-
// Calculate actual operation count (items × targets for each item)
251-
const createOpCount = createOps.reduce(
252-
(sum, op) => sum + op.targets.length,
253-
0,
254-
);
255-
lines.push(
256-
`✨ CREATE (${createOpCount} ${createOpCount === 1 ? "operation" : "operations"}):`,
238+
// Format grouped operations - DRY: data-driven approach eliminates 3x duplication
239+
const operationConfigs = [
240+
{ type: "CREATE" as const, emoji: "✨", prefix: "+" },
241+
{ type: "UPDATE" as const, emoji: "🔄", prefix: "~" },
242+
{ type: "DELETE" as const, emoji: "🗑️ ", prefix: "-" },
243+
];
244+
245+
for (const { type, emoji, prefix } of operationConfigs) {
246+
const ops = Array.from(itemOperations.values()).filter(
247+
(op) => op.operation === type,
257248
);
258-
for (const op of createOps) {
259-
const targetStr =
260-
op.targets.length === targetTools.length
261-
? t("planner.allTargets")
262-
: op.targets.join(", ");
263-
lines.push(` + [${op.type}] ${op.name}${targetStr}`);
264-
}
265-
lines.push("");
266-
}
267249

268-
if (updateOps.length > 0) {
269-
// Calculate actual operation count (items × targets for each item)
270-
const updateOpCount = updateOps.reduce(
271-
(sum, op) => sum + op.targets.length,
272-
0,
273-
);
274-
lines.push(
275-
`🔄 UPDATE (${updateOpCount} ${updateOpCount === 1 ? "operation" : "operations"}):`,
276-
);
277-
for (const op of updateOps) {
278-
const targetStr =
279-
op.targets.length === targetTools.length
280-
? t("planner.allTargets")
281-
: op.targets.join(", ");
282-
lines.push(` ~ [${op.type}] ${op.name}${targetStr}`);
283-
}
284-
lines.push("");
285-
}
250+
if (ops.length === 0) continue;
286251

287-
if (deleteOps.length > 0) {
288252
// Calculate actual operation count (items × targets for each item)
289-
const deleteOpCount = deleteOps.reduce(
290-
(sum, op) => sum + op.targets.length,
291-
0,
292-
);
253+
const opCount = ops.reduce((sum, op) => sum + op.targets.length, 0);
293254
lines.push(
294-
`🗑️ DELETE (${deleteOpCount} ${deleteOpCount === 1 ? "operation" : "operations"}):`,
255+
`${emoji} ${type} (${opCount} ${opCount === 1 ? "operation" : "operations"}):`,
295256
);
296-
for (const op of deleteOps) {
257+
258+
for (const op of ops) {
297259
const targetStr =
298260
op.targets.length === targetTools.length
299261
? t("planner.allTargets")
300262
: op.targets.join(", ");
301-
lines.push(` - [${op.type}] ${op.name}${targetStr}`);
263+
lines.push(` ${prefix} [${op.type}] ${op.name}${targetStr}`);
302264
}
303265
lines.push("");
304266
}

cli/src/types/models.ts

Lines changed: 25 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,69 +4,52 @@
44
*/
55

66
/**
7-
* Command configuration
8-
* Represents a reusable command/script
7+
* Base interface for content-based configuration items
8+
* DRY: Shared structure for Skill, Agent, and Command
99
*/
10-
export interface Command {
11-
/** Command name (directory or file name) */
10+
export interface BaseItem {
11+
/** Item name (directory or file name) */
1212
name: string;
1313
/** Short description (optional, from frontmatter) */
1414
description?: string;
15-
/** Main content from command markdown file */
15+
/** Main content from markdown file */
1616
content: string;
1717
/** Frontmatter metadata */
1818
metadata?: Record<string, unknown>;
1919
/** Support files (relative path -> content) */
2020
supportFiles?: Record<string, string>;
21-
/** SHA256 hash of command content + metadata */
21+
/** SHA256 hash of content + metadata */
2222
hash: string;
2323
}
2424

25-
/**
26-
* MCP server transport type
27-
* - stdio: Standard I/O communication
28-
* - http: HTTP/HTTPS remote server
29-
* - oauth: OAuth-authenticated remote server
30-
*/
31-
export type MCPType = "stdio" | "http" | "oauth";
32-
3325
/**
3426
* Skill configuration
3527
* Represents a reusable instruction template
28+
* Type alias to BaseItem - add specific fields here when needed
3629
*/
37-
export interface Skill {
38-
/** Skill name (directory name) */
39-
name: string;
40-
/** Short description (optional, from frontmatter) */
41-
description?: string;
42-
/** Main content from SKILL.md */
43-
content: string;
44-
/** Frontmatter metadata */
45-
metadata?: Record<string, unknown>;
46-
/** Support files (relative path -> content) */
47-
supportFiles?: Record<string, string>;
48-
/** SHA256 hash of skill content + metadata */
49-
hash: string;
50-
}
30+
export type Skill = BaseItem;
5131

5232
/**
5333
* Agent configuration
5434
* Represents a custom AI agent with specific behaviors and instructions
35+
* Type alias to BaseItem - add specific fields here when needed
5536
*/
56-
export interface Agent {
57-
/** Agent name (directory name) */
58-
name: string;
59-
/** Short description (optional, from frontmatter) */
60-
description?: string;
61-
/** Main content from agent markdown file */
62-
content: string;
63-
/** Frontmatter metadata */
64-
metadata?: Record<string, unknown>;
65-
/** Support files (relative path -> content) */
66-
supportFiles?: Record<string, string>;
67-
/** SHA256 hash of agent content + metadata */
68-
hash: string;
69-
}
37+
export type Agent = BaseItem;
38+
39+
/**
40+
* Command configuration
41+
* Represents a reusable command/script
42+
* Type alias to BaseItem - add specific fields here when needed
43+
*/
44+
export type Command = BaseItem;
45+
46+
/**
47+
* MCP server transport type
48+
* - stdio: Standard I/O communication
49+
* - http: HTTP/HTTPS remote server
50+
* - oauth: OAuth-authenticated remote server
51+
*/
52+
export type MCPType = "stdio" | "http" | "oauth";
7053

7154
/**
7255
* OAuth configuration for OAuth MCP servers

0 commit comments

Comments
 (0)