Skip to content

Commit b9a542b

Browse files
katieschillingclaude
authored andcommitted
feat: add tigris mcp install <agent> command
Auto-configure the Tigris MCP server for AI agents and editors. Reads existing Tigris credentials and writes the MCP server entry into the appropriate config file. Supported agents: claude-code, claude-desktop, cursor, windsurf, vscode Supports --dry-run, --json, and --yes flags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a388d56 commit b9a542b

3 files changed

Lines changed: 255 additions & 0 deletions

File tree

src/lib/mcp/agents.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { homedir, platform } from 'os';
2+
import { join } from 'path';
3+
4+
export interface AgentConfig {
5+
name: string;
6+
configPath: string;
7+
configKey: string;
8+
serverKey: string;
9+
}
10+
11+
const AGENTS: Record<string, () => AgentConfig> = {
12+
'claude-code': () => ({
13+
name: 'Claude Code',
14+
configPath: join(homedir(), '.claude.json'),
15+
configKey: 'mcpServers',
16+
serverKey: 'tigris',
17+
}),
18+
'claude-desktop': () => {
19+
let base: string;
20+
if (platform() === 'win32') {
21+
base = join(process.env.APPDATA || '', 'Claude');
22+
} else if (platform() === 'linux') {
23+
base = join(homedir(), '.config', 'Claude');
24+
} else {
25+
base = join(homedir(), 'Library', 'Application Support', 'Claude');
26+
}
27+
return {
28+
name: 'Claude Desktop',
29+
configPath: join(base, 'claude_desktop_config.json'),
30+
configKey: 'mcpServers',
31+
serverKey: 'tigris',
32+
};
33+
},
34+
cursor: () => ({
35+
name: 'Cursor',
36+
configPath: join(homedir(), '.cursor', 'mcp.json'),
37+
configKey: 'mcpServers',
38+
serverKey: 'tigris',
39+
}),
40+
windsurf: () => ({
41+
name: 'Windsurf',
42+
configPath: join(homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
43+
configKey: 'mcpServers',
44+
serverKey: 'tigris',
45+
}),
46+
vscode: () => {
47+
let base: string;
48+
if (platform() === 'win32') {
49+
base = join(process.env.APPDATA || '', 'Code', 'User');
50+
} else if (platform() === 'linux') {
51+
base = join(homedir(), '.config', 'Code', 'User');
52+
} else {
53+
base = join(homedir(), 'Library', 'Application Support', 'Code', 'User');
54+
}
55+
return {
56+
name: 'VS Code',
57+
configPath: join(base, 'mcp.json'),
58+
configKey: 'mcpServers',
59+
serverKey: 'tigris',
60+
};
61+
},
62+
};
63+
64+
export const VALID_AGENTS = Object.keys(AGENTS);
65+
66+
export function getAgentConfig(agent: string): AgentConfig | null {
67+
const factory = AGENTS[agent];
68+
return factory ? factory() : null;
69+
}

src/lib/mcp/install.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2+
import { dirname } from 'path';
3+
import { getOption } from '../../utils/options.js';
4+
import { getCredentials } from '../../auth/storage.js';
5+
import {
6+
printStart,
7+
printSuccess,
8+
msg,
9+
} from '../../utils/messages.js';
10+
import { isJsonMode, jsonSuccess } from '../../utils/output.js';
11+
import { handleError } from '../../utils/errors.js';
12+
import { confirm } from '../../utils/confirm.js';
13+
import { getAgentConfig, VALID_AGENTS } from './agents.js';
14+
15+
const context = msg('mcp', 'install');
16+
17+
function buildServerEntry(credentials: {
18+
accessKeyId: string;
19+
secretAccessKey: string;
20+
endpoint: string;
21+
}) {
22+
return {
23+
command: 'npx',
24+
args: ['-y', '@tigrisdata/tigris-mcp-server', 'run'],
25+
env: {
26+
AWS_ACCESS_KEY_ID: credentials.accessKeyId,
27+
AWS_SECRET_ACCESS_KEY: credentials.secretAccessKey,
28+
AWS_ENDPOINT_URL_S3: credentials.endpoint,
29+
},
30+
};
31+
}
32+
33+
function readJsonConfig(filePath: string): Record<string, unknown> {
34+
if (!existsSync(filePath)) {
35+
return {};
36+
}
37+
const content = readFileSync(filePath, 'utf8');
38+
try {
39+
return JSON.parse(content);
40+
} catch (err) {
41+
handleError({
42+
message: `Failed to parse ${filePath}: ${err instanceof Error ? err.message : String(err)}. Fix the file or remove it before retrying.`,
43+
});
44+
}
45+
}
46+
47+
export default async function install(options: Record<string, unknown>) {
48+
const agent = getOption<string>(options, ['agent']);
49+
const dryRun = !!getOption<boolean>(options, ['dryRun', 'dry-run']);
50+
const yes = getOption<boolean>(options, ['yes', 'y']);
51+
52+
if (!agent) {
53+
handleError({
54+
message: `Agent is required. Valid agents: ${VALID_AGENTS.join(', ')}`,
55+
});
56+
}
57+
58+
const agentConfig = getAgentConfig(agent);
59+
if (!agentConfig) {
60+
handleError({
61+
message: `Unknown agent "${agent}". Valid agents: ${VALID_AGENTS.join(', ')}`,
62+
});
63+
}
64+
65+
printStart(context, { agent: agentConfig.name });
66+
67+
const credentials = getCredentials();
68+
if (!credentials) {
69+
handleError({
70+
message:
71+
'No Tigris credentials found. Run "tigris login" or "tigris configure" first.',
72+
});
73+
}
74+
75+
const serverEntry = buildServerEntry(credentials);
76+
const configPath = agentConfig.configPath;
77+
const existingConfig = readJsonConfig(configPath);
78+
79+
const mcpServers =
80+
(existingConfig[agentConfig.configKey] as Record<string, unknown>) || {};
81+
const hasExisting = agentConfig.serverKey in mcpServers;
82+
83+
if (hasExisting && !yes) {
84+
console.warn(
85+
`An existing "${agentConfig.serverKey}" entry was found in ${configPath}`
86+
);
87+
const confirmed = await confirm('Overwrite the existing entry?');
88+
if (!confirmed) {
89+
console.log('Aborted');
90+
return;
91+
}
92+
}
93+
94+
const newConfig = {
95+
...existingConfig,
96+
[agentConfig.configKey]: {
97+
...mcpServers,
98+
[agentConfig.serverKey]: serverEntry,
99+
},
100+
};
101+
102+
if (dryRun) {
103+
if (isJsonMode()) {
104+
jsonSuccess({
105+
agent,
106+
configPath,
107+
config: newConfig,
108+
hasExisting,
109+
action: 'would_install',
110+
dryRun: true,
111+
});
112+
} else {
113+
console.log(`[dry-run] Would write to ${configPath}:`);
114+
console.log(JSON.stringify(newConfig, null, 2));
115+
}
116+
return;
117+
}
118+
119+
const parentDir = dirname(configPath);
120+
if (!existsSync(parentDir)) {
121+
mkdirSync(parentDir, { recursive: true });
122+
}
123+
124+
try {
125+
writeFileSync(
126+
configPath,
127+
JSON.stringify(newConfig, null, 2) + '\n',
128+
'utf8'
129+
);
130+
} catch (error) {
131+
handleError({
132+
message: `Failed to write config to ${configPath}: ${error instanceof Error ? error.message : String(error)}`,
133+
});
134+
}
135+
136+
if (isJsonMode()) {
137+
jsonSuccess({
138+
agent,
139+
configPath,
140+
hasExisting,
141+
action: 'installed',
142+
});
143+
} else {
144+
printSuccess(context, { agent: agentConfig.name });
145+
console.log(` Config: ${configPath}`);
146+
if (hasExisting) {
147+
console.log(' (Previous entry was overwritten)');
148+
}
149+
}
150+
}

src/specs.yaml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1533,3 +1533,39 @@ commands:
15331533
type: positional
15341534
required: false
15351535
multiple: true
1536+
1537+
#########################
1538+
# MCP Server Management
1539+
#########################
1540+
- name: mcp
1541+
description: Configure the Tigris MCP server for AI agents and editors
1542+
examples:
1543+
- "tigris mcp install claude-code"
1544+
- "tigris mcp install cursor"
1545+
- "tigris mcp install claude-desktop --dry-run"
1546+
commands:
1547+
- name: install
1548+
description: Configure the Tigris MCP server for a specific AI agent or editor
1549+
alias: i
1550+
examples:
1551+
- "tigris mcp install claude-code"
1552+
- "tigris mcp install cursor"
1553+
- "tigris mcp install claude-desktop"
1554+
- "tigris mcp install windsurf"
1555+
- "tigris mcp install vscode"
1556+
messages:
1557+
onStart: 'Configuring Tigris MCP server for {{agent}}...'
1558+
onSuccess: 'Tigris MCP server configured for {{agent}}'
1559+
onFailure: 'Failed to configure MCP server'
1560+
arguments:
1561+
- name: agent
1562+
type: positional
1563+
required: true
1564+
description: "The AI agent or editor to configure (claude-code, claude-desktop, cursor, windsurf, vscode)"
1565+
options:
1566+
- claude-code
1567+
- claude-desktop
1568+
- cursor
1569+
- windsurf
1570+
- vscode
1571+
default: install

0 commit comments

Comments
 (0)