Skip to content
Draft
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 .github/workflows/token-budget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: token-budget
on:
pull_request:
paths:
- 'server.go'
- 'codemode/tools.go'
- 'scripts/count-tokens.ts'
- '.github/workflows/token-budget.yml'
jobs:
budget:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1
- run: bun install --frozen-lockfile
- run: bun scripts/count-tokens.ts
21 changes: 12 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# syntax=docker/dockerfile:1
FROM debian:stable-slim
COPY --link --from=gcr.io/distroless/base /etc/ssl/certs/ /etc/ssl/certs/
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt/lists \
apt-get update && apt-get install -y --no-install-recommends openssl curl
WORKDIR /app
COPY --link ./nodeops ./
ENTRYPOINT [ "/bin/sh", "-c" ]
FROM golang:1.26-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/createos-mcp .

FROM gcr.io/distroless/static:nonroot
COPY --from=build /out/createos-mcp /createos-mcp
EXPOSE 8080 9090
USER nonroot:nonroot
ENTRYPOINT ["/createos-mcp"]
38 changes: 35 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is an open standard originated by Anthropic that enables AI assistants — Claude, Cursor, GitHub Copilot, Windsurf, Gemini, and others — to securely connect to external tools, APIs, and services through a unified interface. The latest MCP Authorization and Streamable HTTP specifications are fully implemented.

**CreateOS MCP** is a production-grade MCP server that exposes **85+ tools** for full-stack application deployment and infrastructure management on the [CreateOS](https://createos.nodeops.network) platform. Connect it once to your AI coding tool, then deploy projects, manage environments, configure domains, run security scans, analyze deployment metrics, and more — all through natural language.
**CreateOS MCP** is a production-grade MCP server that exposes **10 tools** (7 native + 3 code-mode) for full-stack application deployment and infrastructure management on the [CreateOS](https://createos.nodeops.network) platform. Connect it once to your AI coding tool, then deploy projects, manage environments, configure domains, run security scans, analyze deployment metrics, and more — all through natural language. The full ~100-endpoint CreateOS API is reachable via the `search`/`execute`/`pollJob` Code Mode tools backed by a `workerd` V8 sandbox.

Instead of switching between dashboards, CLIs, and documentation, you stay in your editor and let your AI handle the infrastructure. CreateOS MCP turns prompts like *"deploy my app from this GitHub repo"* or *"scale the staging environment to 3 replicas"* into real actions, executed instantly.

Expand All @@ -25,7 +25,7 @@ Built in Go for performance and reliability, the server supports both **Streamab

## Features

- 🚀 **85+ MCP Tools** — Full coverage of the CreateOS platform API: projects, deployments, environments, domains, templates, and more
- 🚀 **10 MCP Tools** — 7 native fast-path tools plus 3 code-mode tools (search/execute/pollJob) that reach the full ~100-endpoint CreateOS platform API
- 🔌 **9 Supported Clients** — Cursor, VS Code + Copilot, Claude Desktop, Claude Code, Windsurf, Gemini CLI, Gemini Code Assist, Opencode, Zapier, and ElevenLabs
- 🔐 **Secure Authentication** — API key and OAuth 2.0 with Dynamic Client Registration (RFC 7591), Protected Resource Metadata (RFC 9728)
- ⚡ **Dual Transport** — Streamable HTTP for remote access, stdio for local/embedded use
Expand Down Expand Up @@ -200,9 +200,41 @@ npx @modelcontextprotocol/inspector

---

## Code Mode (v2)

CreateOS MCP v2 exposes only 10 tools. The CreateOS API surface (~100 endpoints) is reachable via three code-mode tools:

- `search(code)` — read the OpenAPI spec from a sandboxed JS arrow fn. No network.
- `execute(code)` — run JS that calls `api.<group>.<operationId>(args)` (or `api.raw(method, path, opts)`) against the API. Chained operations run in one sandbox call.
- `pollJob(jobId)` — when an `execute` exceeds 90s it returns `{status: "running", jobId}`. Loop `pollJob` until `status != "running"`.

The sandbox is a `workerd` sidecar running each call in a fresh V8 isolate (Dynamic Worker Loader). No filesystem, no env vars, no ambient `fetch` — only the pre-installed `api` proxy, `console`, and `sleep` are available.

### Native fast-path tools (always available)

`GetQuotas`, `GetSupportedProjectTypes`, `CheckProjectUniqueName`, `CreateProject`, `UploadDeploymentBase64Files`, `GetDeployment`, `CancelDeployment`.

### Example

```js
// execute(code)
async () => {
const { data: dep } = await api.deployments.create({ projectId: "...", source: "upload" });
for (let i = 0; i < 20; i++) {
await sleep(30000);
const s = await api.deployments.get({ id: dep.id });
if (s.data.status === "deployed") return s.data.url;
if (s.data.status === "failed") throw new Error(s.data.error);
}
throw new Error("timeout");
}
```

If the deploy takes > 90s, the call returns `{status:"running", jobId}`; loop `pollJob(jobId)` until done.

## Supported Tools

The server exposes **85+ tools** organized into the following categories:
The server exposes **10 tools** organized into the following categories:

| Category | Tools | Description |
|----------|-------|-------------|
Expand Down
24 changes: 24 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

89 changes: 89 additions & 0 deletions codemode/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package codemode

import (
"context"
"os"
"strings"

"github.com/mark3labs/mcp-go/mcp"
)

// Auth flows:
// - HTTP transport: mcp-go attaches request headers to mcp.CallToolRequest.
// AuthFromRequest reads them directly. HTTP middleware also mirrors them
// into ctx via WithAuthHeaders as a backup path.
// - stdio transport: there is no per-request HTTP header. Auth comes from
// environment variables: CREATEOS_API_KEY and/or CREATEOS_BEARER are
// read at fall-through.
// - AuthFromRequest is the source of truth and cascades: request header →
// ctx headers → env.
type ctxKey int

const authHeadersKey ctxKey = 1

func WithAuthHeaders(ctx context.Context, headers map[string]string) context.Context {
return context.WithValue(ctx, authHeadersKey, headers)
}

func AuthFromContext(ctx context.Context) *AuthCtx {
raw, ok := ctx.Value(authHeadersKey).(map[string]string)
if !ok {
return nil
}
out := authCtxFromHeaders(raw)
if out.APIKey == "" && out.Bearer == "" {
return nil
}
return out
}

// AuthFromEnv reads CREATEOS_API_KEY / CREATEOS_BEARER for stdio transport.
// Returns nil if neither is set.
func AuthFromEnv() *AuthCtx {
out := &AuthCtx{
APIKey: os.Getenv("CREATEOS_API_KEY"),
Bearer: os.Getenv("CREATEOS_BEARER"),
}
if out.APIKey == "" && out.Bearer == "" {
return nil
}
return out
}

// AuthFromRequest extracts caller auth headers from the MCP request, matching
// the native handlers/request.go GetAuthInfo behavior. Falls back to
// AuthFromContext for stdio transport.
func AuthFromRequest(ctx context.Context, req mcp.CallToolRequest) *AuthCtx {
out := &AuthCtx{}
if v := req.Header.Get("X-Api-Key"); v != "" {
out.APIKey = v
}
if h := req.Header.Get("Authorization"); h != "" {
parts := strings.SplitN(h, " ", 2)
if len(parts) == 2 && strings.EqualFold(parts[0], "bearer") && parts[1] != "" {
out.Bearer = parts[1]
}
}
if out.APIKey != "" || out.Bearer != "" {
return out
}
if a := AuthFromContext(ctx); a != nil {
return a
}
return AuthFromEnv()
}

func authCtxFromHeaders(raw map[string]string) *AuthCtx {
out := &AuthCtx{}
for k, v := range raw {
switch strings.ToLower(k) {
case "x-api-key":
out.APIKey = v
case "authorization":
if strings.HasPrefix(strings.ToLower(v), "bearer ") {
out.Bearer = v[len("Bearer "):]
}
}
}
return out
}
88 changes: 88 additions & 0 deletions codemode/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package codemode

import (
"context"
"testing"

"github.com/mark3labs/mcp-go/mcp"
)

func TestAuthFromContext_APIKey(t *testing.T) {
ctx := WithAuthHeaders(context.Background(), map[string]string{"X-Api-Key": "k1"})
got := AuthFromContext(ctx)
if got == nil || got.APIKey != "k1" {
t.Fatalf("got %+v", got)
}
}

func TestAuthFromContext_Bearer(t *testing.T) {
ctx := WithAuthHeaders(context.Background(), map[string]string{"Authorization": "Bearer t1"})
got := AuthFromContext(ctx)
if got == nil || got.Bearer != "t1" {
t.Fatalf("got %+v", got)
}
}

func TestAuthFromContext_Both(t *testing.T) {
ctx := WithAuthHeaders(context.Background(), map[string]string{
"X-Api-Key": "k1",
"Authorization": "Bearer t1",
})
got := AuthFromContext(ctx)
if got == nil || got.APIKey != "k1" || got.Bearer != "t1" {
t.Fatalf("got %+v", got)
}
}

func TestAuthFromContext_Missing(t *testing.T) {
if AuthFromContext(context.Background()) != nil {
t.Fatal("want nil")
}
}

func TestAuthFromRequest_APIKey(t *testing.T) {
req := mcp.CallToolRequest{}
req.Header = map[string][]string{"X-Api-Key": {"k1"}}
got := AuthFromRequest(context.Background(), req)
if got == nil || got.APIKey != "k1" {
t.Fatalf("got %+v", got)
}
}

func TestAuthFromRequest_Bearer(t *testing.T) {
req := mcp.CallToolRequest{}
req.Header = map[string][]string{"Authorization": {"Bearer tok123"}}
got := AuthFromRequest(context.Background(), req)
if got == nil || got.Bearer != "tok123" {
t.Fatalf("got %+v", got)
}
}

func TestAuthFromRequest_FallsBackToEnv(t *testing.T) {
t.Setenv("CREATEOS_API_KEY", "env-key")
t.Setenv("CREATEOS_BEARER", "")
req := mcp.CallToolRequest{}
got := AuthFromRequest(context.Background(), req)
if got == nil || got.APIKey != "env-key" {
t.Fatalf("got %+v", got)
}
}

func TestAuthFromEnv_None(t *testing.T) {
t.Setenv("CREATEOS_API_KEY", "")
t.Setenv("CREATEOS_BEARER", "")
if AuthFromEnv() != nil {
t.Fatal("want nil")
}
}

func TestAuthFromRequest_FallsBackToContext(t *testing.T) {
t.Setenv("CREATEOS_API_KEY", "")
t.Setenv("CREATEOS_BEARER", "")
req := mcp.CallToolRequest{}
ctx := WithAuthHeaders(context.Background(), map[string]string{"X-Api-Key": "ctx-key"})
got := AuthFromRequest(ctx, req)
if got == nil || got.APIKey != "ctx-key" {
t.Fatalf("got %+v", got)
}
}
Loading
Loading