Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,21 @@ Copilot is a Sim-managed service. To use Copilot on a self-hosted instance:
- Go to https://sim.ai → Settings → Copilot and generate a Copilot API key
- Set `COPILOT_API_KEY` environment variable in your self-hosted apps/sim/.env file to that value

## Hosted MCP Servers

Sim can now author and deploy Model Context Protocol servers without leaving the workspace:

1. Scaffold a starter project locally (Reddit search + arXiv summaries included) with
```bash
simstudio mcp init my-research-server
```
2. Push the code to your repo or automation pipeline, then open `/workspace/<id>/mcp` to create a hosted
project, track versions, and trigger deployments.
3. Each deployment spins up a managed MCP endpoint, stores credentials in your workspace, and immediately
appears inside the workflow tool picker.

> **Note:** Set `HOSTED_MCP_BASE_URL` in your `.env` to route hosted deployments through your own reverse
> proxy or edge network.
## Tech Stack

- **Framework**: [Next.js](https://nextjs.org/) (App Router)
Expand All @@ -214,3 +229,4 @@ We welcome contributions! Please see our [Contributing Guide](.github/CONTRIBUTI
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.

<p align="center">Made with ❤️ by the Sim Team</p>

3 changes: 2 additions & 1 deletion apps/sim/app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ BETTER_AUTH_URL=http://localhost:3000

# NextJS (Required)
NEXT_PUBLIC_APP_URL=http://localhost:3000
HOSTED_MCP_BASE_URL=http://localhost:8787

# Security (Required)
ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 32` to generate, used to encrypt environment variables
Expand All @@ -20,4 +21,4 @@ INTERNAL_API_SECRET=your_internal_api_secret # Use `openssl rand -hex 32` to gen
# If left commented out, emails will be logged to console instead

# Local AI Models (Optional)
# OLLAMA_URL=http://localhost:11434 # URL for local Ollama server - uncomment if using local models
# OLLAMA_URL=http://localhost:11434 # URL for local Ollama server - uncomment if using local models
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import type { NextRequest } from 'next/server'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import {
getProjectIdFromRequest,
getDeploymentIdFromRequest,
} from '@/lib/mcp/request-utils'
import {
getMcpServerDeployment,
updateMcpServerDeployment,
} from '@/lib/mcp/deployment-service'
import { createLogger } from '@/lib/logs/console/logger'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'

const logger = createLogger('McpProjectDeploymentDetailsAPI')
const STATUS_VALUES = new Set(['pending', 'deploying', 'active', 'failed', 'decommissioned'])

export const dynamic = 'force-dynamic'

function getIds(request: NextRequest) {
return {
projectId: getProjectIdFromRequest(request),
deploymentId: getDeploymentIdFromRequest(request),
}
}

export const GET = withMcpAuth('read')(async (request: NextRequest, context) => {
const { projectId, deploymentId } = getIds(request)
if (!projectId || !deploymentId) {
return createMcpErrorResponse(
new Error('Missing projectId or deploymentId'),
'Missing parameters',
400
)
}

try {
const deployment = await getMcpServerDeployment(context.workspaceId, projectId, deploymentId)
if (!deployment) {
return createMcpErrorResponse(new Error('Deployment not found'), 'Deployment not found', 404)
}
return createMcpSuccessResponse({ deployment })
} catch (error) {
logger.error(`[${context.requestId}] Failed to fetch deployment ${deploymentId}`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to fetch deployment'),
'Failed to fetch deployment',
500
)
}
})

export const PATCH = withMcpAuth('write')(async (request: NextRequest, context) => {
const { projectId, deploymentId } = getIds(request)
if (!projectId || !deploymentId) {
return createMcpErrorResponse(
new Error('Missing projectId or deploymentId'),
'Missing parameters',
400
)
}

try {
const body = getParsedBody(request) || (await request.json())
const updates: Record<string, any> = {}

if (body.status && STATUS_VALUES.has(body.status)) {
updates.status = body.status
}

if ('endpointUrl' in body) {
updates.endpointUrl = body.endpointUrl ?? null
}

if ('logsUrl' in body) {
updates.logsUrl = body.logsUrl ?? null
}

if ('serverId' in body) {
updates.serverId = body.serverId ?? null
}

if ('rolledBackAt' in body) {
updates.rolledBackAt = body.rolledBackAt ? new Date(body.rolledBackAt) : null
}

if (Object.keys(updates).length === 0) {
return createMcpErrorResponse(new Error('No valid fields to update'), 'No updates', 400)
}

const deployment = await updateMcpServerDeployment(
context.workspaceId,
projectId,
deploymentId,
updates
)
return createMcpSuccessResponse({ deployment })
} catch (error) {
logger.error(`[${context.requestId}] Failed to update deployment ${deploymentId}`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to update deployment'),
error instanceof Error ? error.message : 'Failed to update deployment',
500
)
}
})
73 changes: 73 additions & 0 deletions apps/sim/app/api/mcp/projects/[projectId]/deployments/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { NextRequest } from 'next/server'
import { tasks } from '@trigger.dev/sdk'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { getProjectIdFromRequest } from '@/lib/mcp/request-utils'
import {
createMcpServerDeployment,
listMcpServerDeployments,
} from '@/lib/mcp/deployment-service'
import { createLogger } from '@/lib/logs/console/logger'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'

const logger = createLogger('McpProjectDeploymentsAPI')
export const dynamic = 'force-dynamic'

export const GET = withMcpAuth('read')(async (request: NextRequest, context) => {
const projectId = getProjectIdFromRequest(request)
if (!projectId) {
return createMcpErrorResponse(new Error('Project ID missing from URL'), 'Missing projectId', 400)
}

try {
const deployments = await listMcpServerDeployments(context.workspaceId, projectId)
return createMcpSuccessResponse({ deployments })
} catch (error) {
logger.error(`[${context.requestId}] Failed to list deployments for ${projectId}`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to list deployments'),
'Failed to list deployments',
500
)
}
})

export const POST = withMcpAuth('write')(async (request: NextRequest, context) => {
const projectId = getProjectIdFromRequest(request)
if (!projectId) {
return createMcpErrorResponse(new Error('Project ID missing from URL'), 'Missing projectId', 400)
}

try {
const body = getParsedBody(request) || (await request.json())
if (!body?.versionId) {
return createMcpErrorResponse(new Error('versionId is required'), 'Missing versionId', 400)
}

const deployment = await createMcpServerDeployment({
workspaceId: context.workspaceId,
projectId,
versionId: body.versionId,
environment: body.environment,
region: body.region,
serverId: body.serverId,
deployedBy: context.userId,
})

await tasks.trigger('mcp-server-deploy', {
deploymentId: deployment.id,
projectId,
versionId: body.versionId,
workspaceId: context.workspaceId,
userId: context.userId,
})

return createMcpSuccessResponse({ deployment }, 201)
} catch (error) {
logger.error(`[${context.requestId}] Failed to create deployment for ${projectId}`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to create deployment'),
error instanceof Error ? error.message : 'Failed to create deployment',
500
)
}
})
140 changes: 140 additions & 0 deletions apps/sim/app/api/mcp/projects/[projectId]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type { NextRequest } from 'next/server'
import {
archiveMcpServerProject,
getMcpServerProject,
updateMcpServerProject,
} from '@/lib/mcp/project-service'
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
import { createLogger } from '@/lib/logs/console/logger'
import { getProjectIdFromRequest } from '@/lib/mcp/request-utils'
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'

const logger = createLogger('McpProjectDetailsAPI')
const VISIBILITY_VALUES = new Set(['private', 'workspace', 'public'])
const SOURCE_TYPE_VALUES = new Set(['inline', 'repo', 'package'])
const STATUS_VALUES = new Set(['draft', 'building', 'deploying', 'active', 'failed', 'archived'])

export const dynamic = 'force-dynamic'

export const GET = withMcpAuth('read')(async (request: NextRequest, context) => {
const { workspaceId, requestId } = context
const projectId = getProjectIdFromRequest(request)

try {
if (!projectId) {
return createMcpErrorResponse(new Error('Project ID missing from URL'), 'Missing projectId', 400)
}

const project = await getMcpServerProject(workspaceId, projectId)
if (!project) {
return createMcpErrorResponse(new Error('Project not found'), 'Project not found', 404)
}

return createMcpSuccessResponse({ project })
} catch (error) {
logger.error(`[${requestId}] Failed to fetch MCP project ${projectId}`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to fetch project'),
'Failed to fetch project',
500
)
}
})

export const PATCH = withMcpAuth('write')(async (request: NextRequest, context) => {
const { workspaceId, requestId } = context
const projectId = getProjectIdFromRequest(request)

try {
if (!projectId) {
return createMcpErrorResponse(new Error('Project ID missing from URL'), 'Missing projectId', 400)
}

const body = getParsedBody(request) || (await request.json())
const updates: Record<string, any> = {}

if (typeof body.name === 'string') {
updates.name = body.name
}

if ('description' in body) {
updates.description = body.description ?? null
}

if (body.visibility && VISIBILITY_VALUES.has(body.visibility)) {
updates.visibility = body.visibility
}

if (body.runtime) {
updates.runtime = body.runtime
}

if (body.entryPoint) {
updates.entryPoint = body.entryPoint
}

if ('template' in body) {
updates.template = body.template ?? null
}

if (body.sourceType && SOURCE_TYPE_VALUES.has(body.sourceType)) {
updates.sourceType = body.sourceType
}

if ('repositoryUrl' in body) {
updates.repositoryUrl = body.repositoryUrl ?? null
}

if ('repositoryBranch' in body) {
updates.repositoryBranch = body.repositoryBranch ?? null
}

if (body.environmentVariables && typeof body.environmentVariables === 'object') {
updates.environmentVariables = body.environmentVariables
}

if (body.metadata && typeof body.metadata === 'object') {
updates.metadata = body.metadata
}

if (body.status && STATUS_VALUES.has(body.status)) {
updates.status = body.status
}

if (Object.keys(updates).length === 0) {
return createMcpErrorResponse(new Error('No valid fields to update'), 'No updates', 400)
}

const project = await updateMcpServerProject(workspaceId, projectId, updates)
return createMcpSuccessResponse({ project })
} catch (error) {
logger.error(`[${requestId}] Failed to update MCP project ${projectId}`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to update project'),
error instanceof Error ? error.message : 'Failed to update project',
500
)
}
})

export const DELETE = withMcpAuth('admin')(async (request: NextRequest, context) => {
const { workspaceId, requestId } = context
const projectId = getProjectIdFromRequest(request)

try {
if (!projectId) {
return createMcpErrorResponse(new Error('Project ID missing from URL'), 'Missing projectId', 400)
}

await archiveMcpServerProject(workspaceId, projectId)
logger.info(`[${requestId}] Archived MCP project ${projectId}`)
return createMcpSuccessResponse({ projectId })
} catch (error) {
logger.error(`[${requestId}] Failed to archive MCP project ${projectId}`, error)
return createMcpErrorResponse(
error instanceof Error ? error : new Error('Failed to archive project'),
error instanceof Error ? error.message : 'Failed to archive project',
500
)
}
})
Loading