143 lines
3.5 KiB
TypeScript
143 lines
3.5 KiB
TypeScript
import { v } from "convex/values";
|
|
import { mutation, query } from "./_generated/server";
|
|
|
|
/**
|
|
* User MCP Servers - Cursor-style configuration
|
|
*
|
|
* Allows users to import their MCP configs from Cursor/Claude Desktop
|
|
*/
|
|
|
|
// Add MCP server from cursor config
|
|
export const add = mutation({
|
|
args: {
|
|
name: v.string(),
|
|
url: v.string(),
|
|
headers: v.optional(v.any()),
|
|
env: v.optional(v.any()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const identity = await ctx.auth.getUserIdentity();
|
|
if (!identity) {
|
|
throw new Error("Not authenticated");
|
|
}
|
|
|
|
// Check if MCP with this name already exists for user
|
|
const existing = await ctx.db
|
|
.query("userMCPs")
|
|
.filter((q) =>
|
|
q.and(
|
|
q.eq(q.field("userId"), identity.subject),
|
|
q.eq(q.field("name"), args.name)
|
|
)
|
|
)
|
|
.first();
|
|
|
|
if (existing) {
|
|
// Update existing
|
|
await ctx.db.patch(existing._id, {
|
|
url: args.url,
|
|
headers: args.headers,
|
|
env: args.env,
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
return existing._id;
|
|
}
|
|
|
|
// Create new
|
|
return await ctx.db.insert("userMCPs", {
|
|
userId: identity.subject,
|
|
name: args.name,
|
|
url: args.url,
|
|
headers: args.headers,
|
|
env: args.env,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
},
|
|
});
|
|
|
|
// List user's MCP servers
|
|
export const list = query({
|
|
args: {},
|
|
handler: async (ctx) => {
|
|
const identity = await ctx.auth.getUserIdentity();
|
|
if (!identity) return [];
|
|
|
|
return await ctx.db
|
|
.query("userMCPs")
|
|
.filter((q) => q.eq(q.field("userId"), identity.subject))
|
|
.order("desc")
|
|
.collect();
|
|
},
|
|
});
|
|
|
|
// Remove MCP server
|
|
export const remove = mutation({
|
|
args: { id: v.id("userMCPs") },
|
|
handler: async (ctx, args) => {
|
|
const identity = await ctx.auth.getUserIdentity();
|
|
if (!identity) {
|
|
throw new Error("Not authenticated");
|
|
}
|
|
|
|
const mcp = await ctx.db.get(args.id);
|
|
if (!mcp) {
|
|
throw new Error("MCP not found");
|
|
}
|
|
|
|
if (mcp.userId !== identity.subject) {
|
|
throw new Error("Unauthorized");
|
|
}
|
|
|
|
await ctx.db.delete(args.id);
|
|
return { success: true };
|
|
},
|
|
});
|
|
|
|
// Import from cursor config
|
|
export const importFromCursor = mutation({
|
|
args: {
|
|
config: v.string(), // JSON string of cursor config
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const identity = await ctx.auth.getUserIdentity();
|
|
if (!identity) {
|
|
throw new Error("Not authenticated");
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(args.config);
|
|
|
|
if (!parsed.mcpServers) {
|
|
throw new Error("Invalid config: missing mcpServers field");
|
|
}
|
|
|
|
const imported: Array<{ id: any; name: string }> = [];
|
|
|
|
for (const [name, config] of Object.entries(parsed.mcpServers)) {
|
|
const mcpConfig = config as any;
|
|
|
|
const mcpId = await ctx.db.insert("userMCPs", {
|
|
userId: identity.subject,
|
|
name,
|
|
url: mcpConfig.url || mcpConfig.command || '',
|
|
headers: mcpConfig.headers,
|
|
env: mcpConfig.env,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
});
|
|
|
|
imported.push({ id: mcpId, name });
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
imported: imported.length,
|
|
servers: imported,
|
|
};
|
|
} catch (error) {
|
|
throw new Error(`Failed to import: ${error instanceof Error ? error.message : 'Invalid JSON'}`);
|
|
}
|
|
},
|
|
});
|