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
This commit is contained in:
103
platform/backend/mcp-adapter/README.md
Normal file
103
platform/backend/mcp-adapter/README.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Product OS MCP Adapter
|
||||
|
||||
Exposes Control Plane tools to Continue and other MCP clients.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Continue (MCP client)
|
||||
↓
|
||||
MCP Adapter (this server)
|
||||
↓
|
||||
Control Plane API
|
||||
↓
|
||||
Gemini + Executors
|
||||
```
|
||||
|
||||
## Available Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| `deploy_service` | Deploy a Cloud Run service |
|
||||
| `get_service_status` | Check deployment health |
|
||||
| `get_funnel_analytics` | Get conversion metrics |
|
||||
| `get_top_drivers` | Understand metric drivers |
|
||||
| `generate_marketing_posts` | Create social content |
|
||||
| `chat_with_gemini` | General AI conversation |
|
||||
|
||||
## Setup
|
||||
|
||||
### 1. Install dependencies
|
||||
|
||||
```bash
|
||||
cd platform/backend/mcp-adapter
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2. Make sure Control Plane is running
|
||||
|
||||
```bash
|
||||
cd platform/backend/control-plane
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. Install Continue Extension
|
||||
|
||||
In VS Code/VSCodium:
|
||||
1. Open Extensions (Cmd+Shift+X)
|
||||
2. Search for "Continue"
|
||||
3. Install
|
||||
|
||||
### 4. Configure Continue
|
||||
|
||||
The configuration is already in `.continue/config.yaml`.
|
||||
|
||||
Or manually add to your Continue config:
|
||||
|
||||
```yaml
|
||||
experimental:
|
||||
modelContextProtocolServers:
|
||||
- name: productos
|
||||
command: npx
|
||||
args:
|
||||
- tsx
|
||||
- /path/to/platform/backend/mcp-adapter/src/index.ts
|
||||
env:
|
||||
CONTROL_PLANE_URL: http://localhost:8080
|
||||
```
|
||||
|
||||
### 5. Use in Continue
|
||||
|
||||
1. Open Continue chat (Cmd+L)
|
||||
2. Enable Agent mode (click the robot icon)
|
||||
3. Ask: "Deploy my service to staging"
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CONTROL_PLANE_URL` | `http://localhost:8080` | Control Plane API URL |
|
||||
| `TENANT_ID` | `t_mcp` | Tenant ID for tool calls |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Run directly (for testing)
|
||||
npm run dev
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Run built version
|
||||
npm start
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Test tool listing
|
||||
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | npm run dev
|
||||
|
||||
# Test tool call
|
||||
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_funnel_analytics","arguments":{}}}' | npm run dev
|
||||
```
|
||||
25
platform/backend/mcp-adapter/package.json
Normal file
25
platform/backend/mcp-adapter/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "@productos/mcp-adapter",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "MCP Adapter Server - exposes Control Plane tools to Continue and other MCP clients",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"productos-mcp": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsx src/index.ts",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.0.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
343
platform/backend/mcp-adapter/src/index.ts
Normal file
343
platform/backend/mcp-adapter/src/index.ts
Normal file
@@ -0,0 +1,343 @@
|
||||
#!/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);
|
||||
});
|
||||
14
platform/backend/mcp-adapter/tsconfig.json
Normal file
14
platform/backend/mcp-adapter/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user