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:
2026-01-19 20:34:43 -08:00
commit b6d7148ded
58 changed files with 5365 additions and 0 deletions

View 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
```

View 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"
}
}

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

View 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/**/*"]
}