|
| 1 | +package codemode |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + |
| 6 | + "github.com/mark3labs/mcp-go/mcp" |
| 7 | +) |
| 8 | + |
| 9 | +// MCP Resources + Prompts that expose Code Mode usage to clients. |
| 10 | +// Tools/list alone tells the model the names; these give it the |
| 11 | +// patterns. Pulled on-demand — zero token cost until requested. |
| 12 | + |
| 13 | +const introContent = `# CreateOS MCP — Code Mode v2 |
| 14 | +
|
| 15 | +This server exposes 10 tools: |
| 16 | +
|
| 17 | + Native fast-path (7): |
| 18 | + GetQuotas, GetSupportedProjectTypes, CheckProjectUniqueName, |
| 19 | + CreateProject, UploadDeploymentBase64Files, GetDeployment, |
| 20 | + CancelDeployment |
| 21 | +
|
| 22 | + Code Mode (3): |
| 23 | + search(code) — read-only spec introspection in a JS sandbox |
| 24 | + execute(code) — typed CreateOS API calls in a JS sandbox |
| 25 | + pollJob(jobId) — drain long-running execute jobs |
| 26 | +
|
| 27 | +The full ~100-endpoint API is reachable via execute. Use search first |
| 28 | +to discover an endpoint, then execute to call it. |
| 29 | +
|
| 30 | +## Workflow |
| 31 | +
|
| 32 | +1. search: 'async () => Object.keys(spec.paths).filter(p => p.includes("project"))' |
| 33 | +2. search: 'async () => spec.paths["/v1/projects"]' // inspect schema |
| 34 | +3. execute: 'async () => (await api.projects.listProjects({limit:5})).data' |
| 35 | +
|
| 36 | +If execute exceeds 90s it returns {status:"running", jobId}; loop |
| 37 | +pollJob(jobId) until status != "running". |
| 38 | +
|
| 39 | +## Auth |
| 40 | +
|
| 41 | +Provide X-Api-Key or Authorization: Bearer <token> on the MCP request. |
| 42 | +For stdio transport, set CREATEOS_API_KEY or CREATEOS_BEARER env vars. |
| 43 | +
|
| 44 | +## Sandbox |
| 45 | +
|
| 46 | +workerd V8 isolate. No fs, no env, no ambient fetch. Only: |
| 47 | + spec — OpenAPI document, $refs resolved, descriptions stripped |
| 48 | + api — typed proxy (execute mode only) |
| 49 | + console — log/info/warn/error captured into result.logs |
| 50 | + sleep — async, capped at 60s per call |
| 51 | +
|
| 52 | +## Limits |
| 53 | +
|
| 54 | + search: 5s timeout |
| 55 | + execute: 90s sync window, 600s wall cap, 50 concurrent |
| 56 | + jobs: 30 min TTL after terminal, 500 total cap |
| 57 | + code: 1 MB |
| 58 | + result: 64 KB (oversized truncated with __truncated:true) |
| 59 | +` |
| 60 | + |
| 61 | +const apiShapeContent = `# Code Mode — api shape |
| 62 | +
|
| 63 | +## Typed accessor |
| 64 | +
|
| 65 | + api.<group>.<operationId>(args) |
| 66 | +
|
| 67 | + group = first OpenAPI tag for the operation (lowercase, alnum_). |
| 68 | + operationId = exact operationId from the spec. |
| 69 | + args = single object. Path params by name; query params by name; |
| 70 | + body inferred from remaining keys, or pass {body:{...}} |
| 71 | + explicitly. |
| 72 | +
|
| 73 | + Example: |
| 74 | + api.projects.getProject({id:"p_abc"}) |
| 75 | + api.deployments.createDeployment({projectId:"p_abc", body:{...}}) |
| 76 | +
|
| 77 | +## Escape hatch |
| 78 | +
|
| 79 | + api.raw(method, path, {query, headers, body}) |
| 80 | +
|
| 81 | + Use when an operation lacks an operationId or you need a custom call. |
| 82 | + Path MUST be server-relative (starts with "/"); dot-segments rejected. |
| 83 | +
|
| 84 | +## Return shape |
| 85 | +
|
| 86 | + Success: { status, headers, data } data = parsed JSON or raw text. |
| 87 | + Failure: throws ApiError { |
| 88 | + name: "ApiError", |
| 89 | + status: number, // backend HTTP status |
| 90 | + body: parsed | string, |
| 91 | + path: request path, |
| 92 | + } |
| 93 | +
|
| 94 | + Catch in user code if you want to inspect failures without rejecting: |
| 95 | +
|
| 96 | + try { return (await api.foo.listFoo()).data; } |
| 97 | + catch (e) { return { kind: e.name, status: e.status, body: e.body }; } |
| 98 | +
|
| 99 | +## Discovering operationIds |
| 100 | +
|
| 101 | + search: 'async () => Object.entries(spec.paths).flatMap(([p,m]) => |
| 102 | + Object.entries(m).filter(([k]) => k !== "parameters") |
| 103 | + .map(([method, op]) => ({ path:p, method, op:op.operationId, tags:op.tags })) |
| 104 | + )' |
| 105 | +
|
| 106 | +## Forbidden |
| 107 | +
|
| 108 | + - Dynamic code evaluation primitives are not exposed; user code is |
| 109 | + compiled to a static ESM module. |
| 110 | + - fetch — only api/api.raw reach the backend; no other network. |
| 111 | + - Set-Cookie / Authorization / X-Api-Key in api.raw headers — stripped. |
| 112 | +` |
| 113 | + |
| 114 | +func newCodeModeResource(uri, name, content string) (mcp.Resource, ResourceHandlerFunc) { |
| 115 | + r := mcp.NewResource( |
| 116 | + uri, name, |
| 117 | + mcp.WithMIMEType("text/markdown"), |
| 118 | + mcp.WithResourceDescription("Code Mode reference. Pull on demand."), |
| 119 | + ) |
| 120 | + body := content |
| 121 | + handler := func(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { |
| 122 | + return []mcp.ResourceContents{ |
| 123 | + mcp.TextResourceContents{ |
| 124 | + URI: uri, |
| 125 | + MIMEType: "text/markdown", |
| 126 | + Text: body, |
| 127 | + }, |
| 128 | + }, nil |
| 129 | + } |
| 130 | + return r, handler |
| 131 | +} |
| 132 | + |
| 133 | +// ResourceHandlerFunc mirrors server.ResourceHandlerFunc to avoid an import |
| 134 | +// cycle on the server package; identical signature. |
| 135 | +type ResourceHandlerFunc func(ctx context.Context, req mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) |
| 136 | + |
| 137 | +// NewIntroResource exposes the high-level Code Mode usage doc. |
| 138 | +func NewIntroResource() (mcp.Resource, ResourceHandlerFunc) { |
| 139 | + return newCodeModeResource("code-mode://intro", "Code Mode intro + workflow", introContent) |
| 140 | +} |
| 141 | + |
| 142 | +// NewAPIShapeResource exposes the api proxy / ApiError contract. |
| 143 | +func NewAPIShapeResource() (mcp.Resource, ResourceHandlerFunc) { |
| 144 | + return newCodeModeResource("code-mode://api-shape", "Code Mode api shape + ApiError contract", apiShapeContent) |
| 145 | +} |
| 146 | + |
| 147 | +// Prompts --------------------------------------------------------------- |
| 148 | + |
| 149 | +const deployExamplePromptText = `You are using CreateOS MCP Code Mode v2. |
| 150 | +Goal: deploy a project from an upload and wait for the deployment URL. |
| 151 | +
|
| 152 | +Pattern (single execute call): |
| 153 | +
|
| 154 | + execute({code: 'async () => { |
| 155 | + const { data: dep } = await api.deployments.createDeployment({ |
| 156 | + projectId: PROJECT_ID, |
| 157 | + body: { source: "upload" }, |
| 158 | + }); |
| 159 | + for (let i = 0; i < 20; i++) { |
| 160 | + await sleep(30000); |
| 161 | + const { data: s } = await api.deployments.getDeployment({ id: dep.id }); |
| 162 | + if (s.status === "deployed") return s.url; |
| 163 | + if (s.status === "failed") throw new Error(s.error ?? "deploy failed"); |
| 164 | + } |
| 165 | + throw new Error("deploy did not complete in 10 min"); |
| 166 | + }'}) |
| 167 | +
|
| 168 | +If the call exceeds 90s, the result is {status:"running", jobId}. Loop: |
| 169 | +
|
| 170 | + pollJob({jobId: "j_..."}) |
| 171 | +
|
| 172 | +until status != "running". Each pollJob blocks up to ~90s. |
| 173 | +
|
| 174 | +Replace PROJECT_ID with the real value from CreateProject (native tool) |
| 175 | +or api.projects.listProjects().` |
| 176 | + |
| 177 | +const apiDiscoveryPromptText = `You are using CreateOS MCP Code Mode v2. |
| 178 | +Goal: find which endpoints + parameters are relevant to a task. |
| 179 | +
|
| 180 | +Use search (no network, fast): |
| 181 | +
|
| 182 | + // 1. Find paths matching a keyword |
| 183 | + search({code: 'async () => Object.keys(spec.paths).filter(p => /domain/i.test(p))'}) |
| 184 | +
|
| 185 | + // 2. Inspect the operations on a path |
| 186 | + search({code: 'async () => spec.paths["/v1/domains"]'}) |
| 187 | +
|
| 188 | + // 3. Get the input schema of a specific operation |
| 189 | + search({code: 'async () => spec.paths["/v1/domains"].post.requestBody'}) |
| 190 | +
|
| 191 | + // 4. List all operationIds grouped by tag (= api.<tag>.<operationId>) |
| 192 | + search({code: 'async () => { |
| 193 | + const out = {}; |
| 194 | + for (const [path, methods] of Object.entries(spec.paths)) { |
| 195 | + for (const [m, op] of Object.entries(methods)) { |
| 196 | + if (m === "parameters" || !op.operationId) continue; |
| 197 | + const tag = (op.tags?.[0] ?? "default"); |
| 198 | + (out[tag] ||= []).push({op: op.operationId, method: m.toUpperCase(), path}); |
| 199 | + } |
| 200 | + } |
| 201 | + return out; |
| 202 | + }'}) |
| 203 | +
|
| 204 | +Then call execute with api.<tag>.<operationId>(args).` |
| 205 | + |
| 206 | +func NewDeployExamplePrompt() (mcp.Prompt, PromptHandlerFunc) { |
| 207 | + p := mcp.NewPrompt( |
| 208 | + "code-mode/deploy-example", |
| 209 | + mcp.WithPromptDescription("Code Mode pattern: single execute() that deploys + waits, with pollJob fallback."), |
| 210 | + ) |
| 211 | + handler := func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { |
| 212 | + return &mcp.GetPromptResult{ |
| 213 | + Description: "Deploy + wait pattern using Code Mode execute() and pollJob().", |
| 214 | + Messages: []mcp.PromptMessage{ |
| 215 | + mcp.NewPromptMessage(mcp.RoleUser, mcp.NewTextContent(deployExamplePromptText)), |
| 216 | + }, |
| 217 | + }, nil |
| 218 | + } |
| 219 | + return p, handler |
| 220 | +} |
| 221 | + |
| 222 | +func NewAPIDiscoveryPrompt() (mcp.Prompt, PromptHandlerFunc) { |
| 223 | + p := mcp.NewPrompt( |
| 224 | + "code-mode/api-discovery", |
| 225 | + mcp.WithPromptDescription("Code Mode pattern: 4 search() snippets that walk you from keyword to operationId."), |
| 226 | + ) |
| 227 | + handler := func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { |
| 228 | + return &mcp.GetPromptResult{ |
| 229 | + Description: "Discover the right operationId via search().", |
| 230 | + Messages: []mcp.PromptMessage{ |
| 231 | + mcp.NewPromptMessage(mcp.RoleUser, mcp.NewTextContent(apiDiscoveryPromptText)), |
| 232 | + }, |
| 233 | + }, nil |
| 234 | + } |
| 235 | + return p, handler |
| 236 | +} |
| 237 | + |
| 238 | +// PromptHandlerFunc mirrors server.PromptHandlerFunc. |
| 239 | +type PromptHandlerFunc func(ctx context.Context, req mcp.GetPromptRequest) (*mcp.GetPromptResult, error) |
0 commit comments