Files
master-ai/platform/backend/mcp-adapter/src/index.ts
mawkone b6d7148ded Initial commit: Product OS platform
- 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
2026-01-19 20:34:43 -08:00

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);
});