- Control Plane API with Gemini integration - Executors: Deploy, Analytics, Marketing - MCP Adapter for Continue integration - VSCode/VSCodium extension - Tool registry and run tracking - In-memory storage for local dev - Terraform infrastructure setup
344 lines
9.7 KiB
JavaScript
344 lines
9.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Product OS MCP Adapter Server
|
|
*
|
|
* Exposes Control Plane tools to Continue and other MCP clients.
|
|
*
|
|
* Architecture:
|
|
* Continue (MCP client) → This Adapter → Control Plane → Gemini + Executors
|
|
*
|
|
* This keeps:
|
|
* - Control Plane as the canonical API
|
|
* - Auth/billing/policies centralized
|
|
* - MCP as a compatibility layer
|
|
*/
|
|
|
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
import {
|
|
CallToolRequestSchema,
|
|
ListToolsRequestSchema,
|
|
ToolSchema,
|
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
import { z } from "zod";
|
|
|
|
// Control Plane URL (configurable via env)
|
|
const CONTROL_PLANE_URL = process.env.CONTROL_PLANE_URL || "http://localhost:8080";
|
|
const TENANT_ID = process.env.TENANT_ID || "t_mcp";
|
|
|
|
// ============================================================================
|
|
// Tool Definitions
|
|
// ============================================================================
|
|
|
|
const TOOLS: ToolSchema[] = [
|
|
{
|
|
name: "deploy_service",
|
|
description: "Deploy a Cloud Run service to GCP. Use when the user wants to deploy, ship, or launch code to staging or production.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
service_name: {
|
|
type: "string",
|
|
description: "Name of the service to deploy"
|
|
},
|
|
repo: {
|
|
type: "string",
|
|
description: "Git repository URL (optional, defaults to current workspace)"
|
|
},
|
|
ref: {
|
|
type: "string",
|
|
description: "Git branch, tag, or commit (optional, defaults to main)"
|
|
},
|
|
env: {
|
|
type: "string",
|
|
enum: ["dev", "staging", "prod"],
|
|
description: "Target environment"
|
|
}
|
|
},
|
|
required: ["service_name"]
|
|
}
|
|
},
|
|
{
|
|
name: "get_service_status",
|
|
description: "Check the status of a deployed Cloud Run service. Use when the user asks about service health or deployment status.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
service_name: {
|
|
type: "string",
|
|
description: "Name of the service to check"
|
|
},
|
|
region: {
|
|
type: "string",
|
|
description: "GCP region (defaults to us-central1)"
|
|
}
|
|
},
|
|
required: ["service_name"]
|
|
}
|
|
},
|
|
{
|
|
name: "get_funnel_analytics",
|
|
description: "Get funnel conversion metrics and drop-off analysis. Use when the user asks about funnels, conversions, or user journey.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
funnel_name: {
|
|
type: "string",
|
|
description: "Name of the funnel to analyze (optional)"
|
|
},
|
|
range_days: {
|
|
type: "integer",
|
|
description: "Number of days to analyze (defaults to 30)"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: "get_top_drivers",
|
|
description: "Identify top factors driving a metric. Use when the user asks why something changed or what drives conversions/retention.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
metric: {
|
|
type: "string",
|
|
description: "The metric to analyze (e.g., 'conversion', 'retention', 'churn')"
|
|
},
|
|
range_days: {
|
|
type: "integer",
|
|
description: "Number of days to analyze (defaults to 30)"
|
|
}
|
|
},
|
|
required: ["metric"]
|
|
}
|
|
},
|
|
{
|
|
name: "generate_marketing_posts",
|
|
description: "Generate social media posts for a marketing campaign. Use when the user wants to create content for X, LinkedIn, etc.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
goal: {
|
|
type: "string",
|
|
description: "Campaign goal (e.g., 'product launch', 'feature announcement', 'engagement')"
|
|
},
|
|
product: {
|
|
type: "string",
|
|
description: "Product or feature name"
|
|
},
|
|
channels: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description: "Social channels to generate for (e.g., ['x', 'linkedin'])"
|
|
},
|
|
tone: {
|
|
type: "string",
|
|
description: "Tone of voice (e.g., 'professional', 'casual', 'excited')"
|
|
}
|
|
},
|
|
required: ["goal"]
|
|
}
|
|
},
|
|
{
|
|
name: "chat_with_gemini",
|
|
description: "Have a conversation with Gemini AI about your product, code, or anything else. Use for general questions, code explanation, or ideation.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
message: {
|
|
type: "string",
|
|
description: "The message or question to send to Gemini"
|
|
},
|
|
context: {
|
|
type: "string",
|
|
description: "Additional context (e.g., code snippet, file contents)"
|
|
}
|
|
},
|
|
required: ["message"]
|
|
}
|
|
}
|
|
];
|
|
|
|
// ============================================================================
|
|
// Tool Name Mapping (MCP tool name → Control Plane tool name)
|
|
// ============================================================================
|
|
|
|
const TOOL_MAPPING: Record<string, string> = {
|
|
"deploy_service": "cloudrun.deploy_service",
|
|
"get_service_status": "cloudrun.get_service_status",
|
|
"get_funnel_analytics": "analytics.funnel_summary",
|
|
"get_top_drivers": "analytics.top_drivers",
|
|
"generate_marketing_posts": "marketing.generate_channel_posts",
|
|
};
|
|
|
|
// ============================================================================
|
|
// Control Plane Client
|
|
// ============================================================================
|
|
|
|
async function invokeControlPlaneTool(tool: string, input: any): Promise<any> {
|
|
// Step 1: Invoke the tool
|
|
const invokeResponse = await fetch(`${CONTROL_PLANE_URL}/tools/invoke`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
tool,
|
|
tenant_id: TENANT_ID,
|
|
input,
|
|
dry_run: false
|
|
})
|
|
});
|
|
|
|
if (!invokeResponse.ok) {
|
|
const text = await invokeResponse.text();
|
|
throw new Error(`Control Plane error: ${invokeResponse.status} - ${text}`);
|
|
}
|
|
|
|
const invokeResult = await invokeResponse.json();
|
|
|
|
// Step 2: Fetch the full run to get the output
|
|
const runResponse = await fetch(`${CONTROL_PLANE_URL}/runs/${invokeResult.run_id}`);
|
|
|
|
if (!runResponse.ok) {
|
|
// Fall back to the invoke result if we can't fetch the run
|
|
return invokeResult;
|
|
}
|
|
|
|
return runResponse.json();
|
|
}
|
|
|
|
async function chatWithControlPlane(message: string, context?: string): Promise<any> {
|
|
const messages = [];
|
|
|
|
if (context) {
|
|
messages.push({ role: "user", content: `Context:\n${context}` });
|
|
}
|
|
messages.push({ role: "user", content: message });
|
|
|
|
const response = await fetch(`${CONTROL_PLANE_URL}/chat`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
messages,
|
|
autoExecuteTools: true
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
throw new Error(`Control Plane chat error: ${response.status} - ${text}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
// ============================================================================
|
|
// MCP Server
|
|
// ============================================================================
|
|
|
|
const server = new Server(
|
|
{
|
|
name: "productos-mcp",
|
|
version: "0.1.0",
|
|
},
|
|
{
|
|
capabilities: {
|
|
tools: {},
|
|
},
|
|
}
|
|
);
|
|
|
|
// List available tools
|
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
return { tools: TOOLS };
|
|
});
|
|
|
|
// Handle tool calls
|
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
const { name, arguments: args } = request.params;
|
|
|
|
console.error(`[MCP] Tool called: ${name}`, args);
|
|
|
|
try {
|
|
// Special handling for chat
|
|
if (name === "chat_with_gemini") {
|
|
const result = await chatWithControlPlane(
|
|
(args as any).message,
|
|
(args as any).context
|
|
);
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: result.message || JSON.stringify(result, null, 2)
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
// Map MCP tool name to Control Plane tool name
|
|
const controlPlaneTool = TOOL_MAPPING[name];
|
|
if (!controlPlaneTool) {
|
|
throw new Error(`Unknown tool: ${name}`);
|
|
}
|
|
|
|
// Invoke the Control Plane
|
|
const run = await invokeControlPlaneTool(controlPlaneTool, args);
|
|
|
|
// Format the response
|
|
let responseText = "";
|
|
|
|
if (run.status === "succeeded") {
|
|
responseText = `✅ **${name}** completed successfully\n\n`;
|
|
responseText += "**Result:**\n```json\n";
|
|
responseText += JSON.stringify(run.output, null, 2);
|
|
responseText += "\n```";
|
|
} else if (run.status === "failed") {
|
|
responseText = `❌ **${name}** failed\n\n`;
|
|
responseText += `**Error:** ${run.error?.message || "Unknown error"}`;
|
|
} else {
|
|
responseText = `⏳ **${name}** is ${run.status}\n\n`;
|
|
responseText += `**Run ID:** ${run.run_id}`;
|
|
}
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: responseText
|
|
}
|
|
]
|
|
};
|
|
|
|
} catch (error: any) {
|
|
console.error(`[MCP] Error:`, error);
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: `❌ Error executing ${name}: ${error.message}`
|
|
}
|
|
],
|
|
isError: true
|
|
};
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// Start Server
|
|
// ============================================================================
|
|
|
|
async function main() {
|
|
console.error("[MCP] Product OS MCP Adapter starting...");
|
|
console.error(`[MCP] Control Plane URL: ${CONTROL_PLANE_URL}`);
|
|
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
|
|
console.error("[MCP] Server connected and ready");
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error("[MCP] Fatal error:", error);
|
|
process.exit(1);
|
|
});
|