Skip to content

Commit f4ed82f

Browse files
njb90claude
andcommitted
feat: populate MCP config with Cloudinary servers + add GitHub auth fallback
- Replace empty mcpServers stub with a multi-select picker of real Cloudinary MCP servers (Asset Management, Environment Config, Structured Metadata, Analysis, MediaFlows) - Merges into existing config file rather than overwriting, using the correct root key ("servers" for VS Code, "mcpServers" for others) - VS Code format uses ${env:VAR} references; Cursor/others use plain placeholder strings - Add silent GitHub auth token fallback to githubFetchJson so the private skills repo works during development; degrades gracefully to unauthenticated once the repo is public Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 58ef514 commit f4ed82f

1 file changed

Lines changed: 183 additions & 8 deletions

File tree

src/commands/configureAiTools.ts

Lines changed: 183 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ import * as vscode from "vscode";
44

55
type EditorType = "cursor" | "vscode" | "windsurf" | "antigravity" | "unknown";
66

7+
type McpServerDef = {
8+
label: string;
9+
description: string;
10+
key: string;
11+
/** Config entry for Cursor / windsurf / antigravity / Claude (mcpServers format) */
12+
cursorConfig: Record<string, unknown>;
13+
/** Config entry for VS Code (servers format, "servers" root key) */
14+
vscodeConfig: Record<string, unknown>;
15+
};
16+
717
type SkillInfo = {
818
name: string;
919
description: string;
@@ -51,9 +61,18 @@ function getMcpFilePath(editor: EditorType): string {
5161
const SKILLS_BASE = "https://api.github.com/repos/cloudinary-devs/skills/contents";
5262

5363
async function githubFetchJson<T>(url: string): Promise<T> {
54-
const response = await fetch(url, {
55-
headers: { Accept: "application/vnd.github+json" },
56-
});
64+
const headers: Record<string, string> = { Accept: "application/vnd.github+json" };
65+
66+
try {
67+
const session = await vscode.authentication.getSession("github", ["repo"], { silent: true });
68+
if (session) {
69+
headers["Authorization"] = `Bearer ${session.accessToken}`;
70+
}
71+
} catch {
72+
// auth not available — proceed unauthenticated
73+
}
74+
75+
const response = await fetch(url, { headers });
5776
if (!response.ok) {
5877
throw new Error(`GitHub API ${response.status}: ${url}`);
5978
}
@@ -281,20 +300,176 @@ async function installForCopilot(
281300
}
282301
}
283302

303+
// ── MCP Server definitions ────────────────────────────────────────────────────
304+
305+
const MCP_SERVERS: McpServerDef[] = [
306+
{
307+
label: "Cloudinary Asset Management",
308+
description: "Browse, upload, and manage media assets",
309+
key: "cloudinary-asset-mgmt",
310+
cursorConfig: {
311+
command: "npx",
312+
args: ["-y", "@cloudinary/asset-management", "mcp", "start"],
313+
env: {
314+
CLOUDINARY_CLOUD_NAME: "your_cloud_name",
315+
CLOUDINARY_API_KEY: "your_api_key",
316+
CLOUDINARY_API_SECRET: "your_api_secret",
317+
},
318+
},
319+
vscodeConfig: {
320+
type: "stdio",
321+
command: "npx",
322+
args: ["-y", "@cloudinary/asset-management", "mcp", "start"],
323+
env: {
324+
CLOUDINARY_CLOUD_NAME: "${env:CLOUDINARY_CLOUD_NAME}",
325+
CLOUDINARY_API_KEY: "${env:CLOUDINARY_API_KEY}",
326+
CLOUDINARY_API_SECRET: "${env:CLOUDINARY_API_SECRET}",
327+
},
328+
},
329+
},
330+
{
331+
label: "Cloudinary Environment Config",
332+
description: "Configure upload presets, transformations, and settings",
333+
key: "cloudinary-env-config",
334+
cursorConfig: {
335+
command: "npx",
336+
args: ["-y", "@cloudinary/environment-config", "mcp", "start"],
337+
env: {
338+
CLOUDINARY_CLOUD_NAME: "your_cloud_name",
339+
CLOUDINARY_API_KEY: "your_api_key",
340+
CLOUDINARY_API_SECRET: "your_api_secret",
341+
},
342+
},
343+
vscodeConfig: {
344+
type: "stdio",
345+
command: "npx",
346+
args: ["-y", "@cloudinary/environment-config", "mcp", "start"],
347+
env: {
348+
CLOUDINARY_CLOUD_NAME: "${env:CLOUDINARY_CLOUD_NAME}",
349+
CLOUDINARY_API_KEY: "${env:CLOUDINARY_API_KEY}",
350+
CLOUDINARY_API_SECRET: "${env:CLOUDINARY_API_SECRET}",
351+
},
352+
},
353+
},
354+
{
355+
label: "Cloudinary Structured Metadata",
356+
description: "Manage structured metadata fields and values",
357+
key: "cloudinary-smd",
358+
cursorConfig: {
359+
command: "npx",
360+
args: ["-y", "@cloudinary/structured-metadata", "mcp", "start"],
361+
env: {
362+
CLOUDINARY_CLOUD_NAME: "your_cloud_name",
363+
CLOUDINARY_API_KEY: "your_api_key",
364+
CLOUDINARY_API_SECRET: "your_api_secret",
365+
},
366+
},
367+
vscodeConfig: {
368+
type: "stdio",
369+
command: "npx",
370+
args: ["-y", "@cloudinary/structured-metadata", "mcp", "start"],
371+
env: {
372+
CLOUDINARY_CLOUD_NAME: "${env:CLOUDINARY_CLOUD_NAME}",
373+
CLOUDINARY_API_KEY: "${env:CLOUDINARY_API_KEY}",
374+
CLOUDINARY_API_SECRET: "${env:CLOUDINARY_API_SECRET}",
375+
},
376+
},
377+
},
378+
{
379+
label: "Cloudinary Analysis",
380+
description: "AI-powered image and video analysis",
381+
key: "cloudinary-analysis",
382+
cursorConfig: {
383+
command: "npx",
384+
args: ["-y", "@cloudinary/analysis", "mcp", "start"],
385+
env: {
386+
CLOUDINARY_CLOUD_NAME: "your_cloud_name",
387+
CLOUDINARY_API_KEY: "your_api_key",
388+
CLOUDINARY_API_SECRET: "your_api_secret",
389+
},
390+
},
391+
vscodeConfig: {
392+
type: "stdio",
393+
command: "npx",
394+
args: ["-y", "@cloudinary/analysis", "mcp", "start"],
395+
env: {
396+
CLOUDINARY_CLOUD_NAME: "${env:CLOUDINARY_CLOUD_NAME}",
397+
CLOUDINARY_API_KEY: "${env:CLOUDINARY_API_KEY}",
398+
CLOUDINARY_API_SECRET: "${env:CLOUDINARY_API_SECRET}",
399+
},
400+
},
401+
},
402+
{
403+
label: "MediaFlows",
404+
description: "AI-powered media workflows and automation",
405+
key: "mediaflows",
406+
cursorConfig: {
407+
url: "https://mediaflows.mcp.cloudinary.com/v2/mcp",
408+
headers: {
409+
"cld-cloud-name": "your_cloud_name",
410+
"cld-api-key": "your_api_key",
411+
"cld-secret": "your_api_secret",
412+
},
413+
},
414+
vscodeConfig: {
415+
url: "https://mediaflows.mcp.cloudinary.com/v2/mcp",
416+
headers: {
417+
"cld-cloud-name": "your_cloud_name",
418+
"cld-api-key": "your_api_key",
419+
"cld-secret": "your_api_secret",
420+
},
421+
},
422+
},
423+
];
424+
284425
// ── MCP Config ───────────────────────────────────────────────────────────────
285426

286427
async function createMcpConfig(
287428
rootUri: vscode.Uri,
429+
editor: EditorType,
288430
mcpFilePath: string,
289431
createdFiles: string[]
290432
): Promise<void> {
433+
const selected = await vscode.window.showQuickPick(
434+
MCP_SERVERS.map((s) => ({ label: s.label, description: s.description, picked: true })),
435+
{ canPickMany: true, placeHolder: "Select MCP servers to configure" }
436+
);
437+
if (!selected || selected.length === 0) { return; }
438+
439+
const selectedDefs = selected
440+
.map((item) => MCP_SERVERS.find((s) => s.label === item.label))
441+
.filter((s): s is McpServerDef => s !== undefined);
442+
291443
const mcpUri = vscode.Uri.joinPath(rootUri, mcpFilePath);
292-
const written = await writeWithOverwriteCheck(
444+
const isVscode = editor === "vscode";
445+
const rootKey = isVscode ? "servers" : "mcpServers";
446+
447+
// Read and merge into existing config if present
448+
let config: Record<string, unknown> = {};
449+
try {
450+
const bytes = await vscode.workspace.fs.readFile(mcpUri);
451+
config = JSON.parse(Buffer.from(bytes).toString("utf-8"));
452+
} catch {
453+
// new file
454+
}
455+
456+
if (!config[rootKey] || typeof config[rootKey] !== "object") {
457+
config[rootKey] = {};
458+
}
459+
const servers = config[rootKey] as Record<string, unknown>;
460+
461+
for (const def of selectedDefs) {
462+
servers[def.key] = isVscode ? def.vscodeConfig : def.cursorConfig;
463+
}
464+
465+
await ensureDir(vscode.Uri.joinPath(mcpUri, ".."));
466+
await vscode.workspace.fs.writeFile(
293467
mcpUri,
294-
JSON.stringify({ mcpServers: {} }, null, 2),
295-
mcpFilePath
468+
Buffer.from(JSON.stringify(config, null, 2), "utf-8")
296469
);
297-
if (written) { createdFiles.push(mcpFilePath); }
470+
if (!createdFiles.includes(mcpFilePath)) {
471+
createdFiles.push(mcpFilePath);
472+
}
298473
}
299474

300475
// ── Command registration ─────────────────────────────────────────────────────
@@ -394,7 +569,7 @@ function registerConfigureAiTools(context: vscode.ExtensionContext): void {
394569
// ── Step 3: MCP config flow ────────────────────────────────────────────
395570
if (options.some((o) => o.label === "MCP Config")) {
396571
const editor = detectEditor();
397-
await createMcpConfig(rootUri, getMcpFilePath(editor), createdFiles);
572+
await createMcpConfig(rootUri, editor, getMcpFilePath(editor), createdFiles);
398573
}
399574

400575
// ── Step 4: feedback ───────────────────────────────────────────────────

0 commit comments

Comments
 (0)