From 2fd6dbd339455e3d80102660f7fff374dc9bf417 Mon Sep 17 00:00:00 2001 From: ProgrammerIn-wonderland Date: Wed, 3 Jun 2026 12:05:56 -0400 Subject: [PATCH] add workers and better prompt engineering --- src/mcp-connector/README.md | 42 ++++- src/mcp-connector/mcpb/manifest.json | 23 ++- src/mcp-connector/src/mcp.js | 8 +- src/mcp-connector/src/tools.js | 267 ++++++++++++++++++++++++--- 4 files changed, 298 insertions(+), 42 deletions(-) diff --git a/src/mcp-connector/README.md b/src/mcp-connector/README.md index 8971c053f..37a1957d2 100644 --- a/src/mcp-connector/README.md +++ b/src/mcp-connector/README.md @@ -2,7 +2,7 @@ An [MCP](https://modelcontextprotocol.io) server that runs on Cloudflare Workers and lets **anyone use their own Puter auth token** to drive their Puter account's -filesystem and subdomains from an MCP client (Claude, Cursor, etc.). +filesystem and static website hosting from an MCP client (Claude, Cursor, etc.). The Worker stores **no credentials of its own**. Every request runs as the caller, authenticated either by a `Authorization: Bearer ` header or by an @@ -21,14 +21,38 @@ OAuth "Sign in with Puter" flow the Worker hosts itself (see | `fs_delete` | Delete a file or directory (recursive by default). | | `fs_readdir` | List the entries of a directory. | -### Subdomains +### Hosting (static websites) +Publishing a website in Puter means creating a hosting subdomain served at +`https://.puter.site`, backed by a directory in your Puter filesystem. + | Tool | Description | | --- | --- | -| `subdomains_list` | List the caller's subdomains. | -| `subdomains_get` | Get a subdomain by name. | -| `subdomains_create` | Create a subdomain (optionally pointing at a `root_dir`). | -| `subdomains_update` | Update a subdomain's `root_dir`. | -| `subdomains_delete` | Delete a subdomain. | +| `hosting_list` | List the caller's published websites (hosting subdomains). | +| `hosting_get` | Get a website by its subdomain. | +| `hosting_create` | Publish a website on a subdomain (optionally pointing at a `root_dir`). | +| `hosting_update` | Re-point a website at a different `root_dir`. | +| `hosting_delete` | Unpublish a website (deletes the subdomain, not the files). | + +### Workers (serverless functions) +Puter Workers are serverless JavaScript functions deployed from a file in your +Puter filesystem. The worker file defines handlers on the global `router` object +and has the full puter.js SDK available as `puter` — they are designed to be used +**with puter.js and Puter authentication**. Update a worker by writing new code to +its associated file (there is no separate update call). + +| Tool | Description | +| --- | --- | +| `workers_create` | Deploy a worker from a JS file; returns its public URL. | +| `workers_list` | List the caller's deployed workers. | +| `workers_get` | Get a worker (name, URL, source file) by name. | +| `workers_exec` | Call a worker over HTTP as the authenticated user. | +| `workers_delete` | Undeploy a worker (leaves its source file in place). | + +### Documentation +| Tool | Description | +| --- | --- | +| `puter_docs_index` | Load the puter.js docs index (every topic + path) from `docs.puter.com/llms.txt`. | +| `puter_docs_get` | Fetch a specific docs page as Markdown by topic path (e.g. `Workers/router`). | ### Paths Tools pass paths straight to puter.js, so the usual conventions apply: absolute @@ -203,7 +227,7 @@ curl -s $URL -H "Authorization: Bearer $TOKEN" -H 'content-type: application/jso "params":{"name":"fs_write_file","arguments":{"path":"~/Desktop/hello.txt","content":"hi from MCP"}} }' -# list subdomains +# list hosted websites curl -s $URL -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \ - -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"subdomains_list","arguments":{}}}' + -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"hosting_list","arguments":{}}}' ``` diff --git a/src/mcp-connector/mcpb/manifest.json b/src/mcp-connector/mcpb/manifest.json index 7b113afe0..a17c8dd57 100644 --- a/src/mcp-connector/mcpb/manifest.json +++ b/src/mcp-connector/mcpb/manifest.json @@ -3,8 +3,8 @@ "name": "puter-mcp-connector", "display_name": "Puter", "version": "0.1.0", - "description": "Use your own Puter account's filesystem and subdomains over MCP.", - "long_description": "Connects an MCP client to a deployed Puter MCP Worker. Authenticates with your own Puter token (no shared credentials) and exposes tools to read/write/stat files, make directories, delete entries, and manage subdomains. The bundle runs a tiny local stdio<->HTTP proxy that forwards JSON-RPC to your Worker with your token attached.", + "description": "Use your own Puter account's filesystem, website hosting, and serverless workers over MCP.", + "long_description": "Connects an MCP client to a deployed Puter MCP Worker. Authenticates with your own Puter token (no shared credentials) and exposes tools to read/write/stat files, make directories, delete entries, host static websites, and deploy/run serverless Puter Workers (which are built with puter.js and Puter authentication). It also ships puter.js documentation tools so an agent can look up the exact API before writing worker or SDK code. The bundle runs a tiny local stdio<->HTTP proxy that forwards JSON-RPC to your Worker with your token attached.", "author": { "name": "Puter Technologies Inc.", "url": "https://puter.com" @@ -12,7 +12,7 @@ "homepage": "https://puter.com", "documentation": "https://github.com/HeyPuter/puter/tree/main/src/mcp-connector", "license": "AGPL-3.0-only", - "keywords": ["puter", "filesystem", "hosting", "subdomains", "fs"], + "keywords": ["puter", "filesystem", "hosting", "subdomains", "workers", "serverless", "puter.js", "fs"], "server": { "type": "node", "entry_point": "server/index.cjs", @@ -33,11 +33,18 @@ { "name": "fs_mkdir", "description": "Create a directory (optionally creating missing parents)." }, { "name": "fs_delete", "description": "Delete a file or directory (recursive by default)." }, { "name": "fs_readdir", "description": "List the entries of a directory." }, - { "name": "subdomains_list", "description": "List the caller's subdomains." }, - { "name": "subdomains_get", "description": "Get a subdomain by name." }, - { "name": "subdomains_create", "description": "Create a subdomain (optionally pointing at a root_dir)." }, - { "name": "subdomains_update", "description": "Update a subdomain's root directory." }, - { "name": "subdomains_delete", "description": "Delete a subdomain." } + { "name": "hosting_list", "description": "List the caller's published websites (hosting subdomains, served at .puter.site)." }, + { "name": "hosting_get", "description": "Get a published website by its subdomain." }, + { "name": "hosting_create", "description": "Publish a static website on a subdomain (optionally pointing at a root_dir)." }, + { "name": "hosting_update", "description": "Re-point a website at a different root directory." }, + { "name": "hosting_delete", "description": "Unpublish a website (deletes the subdomain, not the files)." }, + { "name": "workers_create", "description": "Deploy a serverless Puter Worker from a JS file (uses the global router + puter.js); returns its public URL. Update by writing to the same file." }, + { "name": "workers_list", "description": "List the caller's deployed serverless workers." }, + { "name": "workers_get", "description": "Get a deployed worker (name, URL, source file) by name." }, + { "name": "workers_exec", "description": "Call a deployed worker over HTTP as the authenticated user." }, + { "name": "workers_delete", "description": "Undeploy a worker (leaves its source file in place)." }, + { "name": "puter_docs_index", "description": "Load the puter.js documentation index (topics + paths) from docs.puter.com/llms.txt." }, + { "name": "puter_docs_get", "description": "Fetch a specific puter.js documentation page as Markdown by topic path (e.g. Workers/router)." } ], "user_config": { "server_url": { diff --git a/src/mcp-connector/src/mcp.js b/src/mcp-connector/src/mcp.js index 919771eee..3d79eb538 100644 --- a/src/mcp-connector/src/mcp.js +++ b/src/mcp-connector/src/mcp.js @@ -68,7 +68,11 @@ async function handleMessage(msg, userPuter) { serverInfo: SERVER_INFO, instructions: 'Puter MCP server. Authenticate with your own Puter token via the ' + - 'Authorization: Bearer header. Provides filesystem and subdomain tools.', + 'Authorization: Bearer header. Provides filesystem tools (fs_*), ' + + 'static website hosting tools (hosting_*, served at .puter.site), ' + + 'serverless worker tools (workers_*), and puter.js documentation tools ' + + '(puter_docs_*). Puter Workers are built WITH puter.js and Puter authentication: ' + + 'before writing worker code, call puter_docs_get with path "Workers/router".', }); } @@ -193,7 +197,7 @@ function mcpInfo() { return { name: 'puter-mcp', description: - 'MCP server for Puter filesystem and subdomain operations. POST JSON-RPC to ' + + 'MCP server for Puter filesystem, static website hosting, and serverless worker operations. POST JSON-RPC to ' + 'this endpoint with your Puter token in the Authorization: Bearer header.', transport: 'streamable-http', tools: listTools().map((t) => t.name), diff --git a/src/mcp-connector/src/tools.js b/src/mcp-connector/src/tools.js index c7eb404df..962c42d75 100644 --- a/src/mcp-connector/src/tools.js +++ b/src/mcp-connector/src/tools.js @@ -54,13 +54,47 @@ async function decodeReadResult(result, encoding) { return { content: new TextDecoder().decode(bytes), encoding: 'utf8', bytes: bytes.length }; } +// ----- puter.js documentation fetching ------------------------------------- +// The puter_docs_* tools pull authoritative docs straight from docs.puter.com so +// an agent writes correct worker / SDK code instead of guessing the API. +const DOCS_HOST = 'docs.puter.com'; +const DOCS_INDEX_URL = `https://${DOCS_HOST}/llms.txt`; + +/** Resolve a docs topic/path to a canonical https://docs.puter.com/.../index.md URL. */ +function resolveDocUrl(pathOrTopic) { + let p = String(pathOrTopic || '').trim(); + if (!p || p === 'llms' || p === 'llms.txt') return DOCS_INDEX_URL; + // Accept a full URL, but only on the docs host (avoid SSRF to arbitrary hosts). + if (/^https?:\/\//i.test(p)) { + const u = new URL(p); + if (u.hostname !== DOCS_HOST) { + throw new Error(`Only ${DOCS_HOST} documentation URLs are allowed.`); + } + return u.toString(); + } + // Normalize a topic slug like "Workers/router" or "Workers/router/index.md". + p = p.replace(/^\/+|\/+$/g, '').replace(/\/index\.md$/i, '').replace(/\.md$/i, ''); + if (!p) return DOCS_INDEX_URL; + return `https://${DOCS_HOST}/${p}/index.md`; +} + +/** Fetch a docs page as text, throwing a readable error on failure. */ +async function fetchDocText(url) { + const resp = await fetch(url, { headers: { accept: 'text/markdown, text/plain, */*' } }); + if (!resp.ok) { + throw new Error(`Failed to fetch Puter docs (HTTP ${resp.status}) from ${url}`); + } + return resp.text(); +} + export const TOOLS = [ // ----- filesystem ------------------------------------------------------ { name: 'fs_read_file', description: 'Read the contents of a file in Puter. Returns UTF-8 text by default; ' + - 'pass encoding="base64" for binary files. Supports optional byte offset/length.', + 'pass encoding="base64" for binary files. Supports optional byte offset/length. ' + + 'Equivalent to PuterJS puter.fs.read(path).', inputSchema: { type: 'object', properties: { @@ -82,7 +116,7 @@ export const TOOLS = [ }, { name: 'fs_stat', - description: 'Get metadata (name, size, type, timestamps, uid) for a file or directory in Puter.', + description: 'Get metadata (name, size, type, timestamps, uid) for a file or directory in Puter. Equivalent to PuterJS puter.fs.stat(path).', inputSchema: { type: 'object', properties: { @@ -99,7 +133,7 @@ export const TOOLS = [ name: 'fs_write_file', description: 'Write (create or overwrite) a file in Puter. Provide content as UTF-8 text, ' + - 'or set encoding="base64" to write binary data.', + 'or set encoding="base64" to write binary data. Equivalent to PuterJS puter.fs.write(path, data).', inputSchema: { type: 'object', properties: { @@ -133,7 +167,7 @@ export const TOOLS = [ }, { name: 'fs_mkdir', - description: 'Create a directory in Puter (optionally creating missing parents).', + description: 'Create a directory in Puter (optionally creating missing parents). Equivalent to PuterJS puter.fs.mkdir(path).', inputSchema: { type: 'object', properties: { @@ -152,7 +186,7 @@ export const TOOLS = [ }, { name: 'fs_delete', - description: 'Delete a file or directory in Puter. Directories are removed recursively by default.', + description: 'Delete a file or directory in Puter. Directories are removed recursively by default. Equivalent to PuterJS puter.fs.delete(path).', inputSchema: { type: 'object', properties: { @@ -171,7 +205,7 @@ export const TOOLS = [ }, { name: 'fs_readdir', - description: 'List the entries (files and subdirectories) of a directory in Puter.', + description: 'List the entries (files and subdirectories) of a directory in Puter. Equivalent to PuterJS puter.fs.readdir(path).', inputSchema: { type: 'object', properties: { @@ -184,22 +218,34 @@ export const TOOLS = [ }, }, - // ----- subdomains (puter.hosting) -------------------------------------- + // ----- hosting / static websites (puter.hosting) ----------------------- + // In Puter, "hosting" means publishing a static website. Each website lives + // at a subdomain of puter.site (e.g. "my-site" -> https://my-site.puter.site) + // and is backed by a directory in the user's Puter filesystem. These tools + // are how an agent puts files online: write the site's files with fs_write_file, + // then hosting_create a subdomain pointing at that directory. { - name: 'subdomains_list', - description: 'List all subdomains owned by the authenticated Puter user.', + name: 'hosting_list', + description: + 'List all websites (hosting subdomains) the authenticated Puter user has published. ' + + 'Each entry includes the subdomain (served at https://.puter.site) and the ' + + 'Puter directory it is hosted from. Use this to discover existing sites before creating ' + + 'or updating one. Equivalent to PuterJS puter.hosting.list().', inputSchema: { type: 'object', properties: {} }, async handler(puter) { return puter.hosting.list(); }, }, { - name: 'subdomains_get', - description: 'Get a single subdomain (and its root directory) by name.', + name: 'hosting_get', + description: + 'Get a single published website (hosting subdomain) by its subdomain label, including ' + + 'the Puter directory it serves from. The live site is reachable at ' + + 'https://.puter.site. Equivalent to PuterJS puter.hosting.get(subdomain).', inputSchema: { type: 'object', properties: { - subdomain: { type: 'string', description: 'The subdomain label, e.g. "my-site".' }, + subdomain: { type: 'string', description: 'The subdomain label, e.g. "my-site" (without the .puter.site suffix).' }, }, required: ['subdomain'], }, @@ -208,19 +254,24 @@ export const TOOLS = [ }, }, { - name: 'subdomains_create', + name: 'hosting_create', description: - 'Create a new subdomain. Optionally point it at a Puter directory (root_dir) to host a static site.', + 'Publish a new static website by creating a hosting subdomain. The site goes live at ' + + 'https://.puter.site. Point it at a Puter directory (root_dir) to serve that ' + + "directory's files (e.g. an index.html) as a website; omit root_dir to reserve the " + + 'subdomain and attach a directory later with hosting_update. Typical flow: fs_mkdir a ' + + 'directory, fs_write_file your index.html into it, then hosting_create with that root_dir. ' + + 'Equivalent to PuterJS puter.hosting.create(subdomain, root_dir).', inputSchema: { type: 'object', properties: { subdomain: { type: 'string', - description: 'Subdomain label (lowercase letters, digits, hyphens; max 64 chars).', + description: 'Subdomain label for the site (lowercase letters, digits, hyphens; max 64 chars). The site will be served at https://.puter.site.', }, root_dir: { type: 'string', - description: 'Optional Puter directory path the subdomain serves from.', + description: 'Puter directory path whose files are served as the website (e.g. "/me/my-site"). Omit to create the subdomain without content for now.', }, }, required: ['subdomain'], @@ -232,15 +283,18 @@ export const TOOLS = [ }, }, { - name: 'subdomains_update', - description: "Update an existing subdomain's root directory.", + name: 'hosting_update', + description: + 'Re-point an existing website (hosting subdomain) at a different Puter directory, changing ' + + 'which files https://.puter.site serves. Use this to attach content to a bare ' + + 'subdomain or to swap the served directory. Equivalent to PuterJS puter.hosting.update(subdomain, root_dir).', inputSchema: { type: 'object', properties: { - subdomain: { type: 'string', description: 'The subdomain label to update.' }, + subdomain: { type: 'string', description: 'The subdomain label of the site to update.' }, root_dir: { type: 'string', - description: 'New Puter directory path to serve from.', + description: 'New Puter directory path to serve the website from.', }, }, required: ['subdomain', 'root_dir'], @@ -250,12 +304,15 @@ export const TOOLS = [ }, }, { - name: 'subdomains_delete', - description: 'Delete a subdomain by name.', + name: 'hosting_delete', + description: + 'Unpublish a website by deleting its hosting subdomain. This takes ' + + 'https://.puter.site offline but does NOT delete the underlying Puter directory ' + + 'or its files. Equivalent to PuterJS puter.hosting.delete(subdomain).', inputSchema: { type: 'object', properties: { - subdomain: { type: 'string', description: 'The subdomain label to delete.' }, + subdomain: { type: 'string', description: 'The subdomain label of the site to unpublish.' }, }, required: ['subdomain'], }, @@ -263,6 +320,170 @@ export const TOOLS = [ return puter.hosting.delete(subdomain); }, }, + + // ----- serverless workers (puter.workers) ------------------------------ + // Puter Workers are serverless JavaScript functions deployed from a file in + // the user's Puter filesystem. The worker file defines handlers on the global + // `router` object (router.get/router.post/...) and has the full puter.js SDK + // available as `puter`, authenticated as the deployer (`me.puter`) or, when + // invoked via puter.workers.exec(), the calling user (`user.puter`). Workers + // are designed to be used WITH puter.js and Puter authentication, NOT as plain + // standalone HTTP handlers — always read the router guide (puter_docs_get + // "Workers/router") before writing worker code. + { + name: 'workers_create', + description: + 'Deploy a serverless Puter Worker from a JavaScript file in the Puter filesystem and ' + + 'return its public URL. The worker file MUST define handlers on the global `router` object ' + + '(router.get/router.post/router.put/router.delete) and may use the global puter.js SDK ' + + '(`puter`) for storage, KV, AI, and more — authenticated as you, the deployer. Puter Workers ' + + 'are designed to be used WITH puter.js and Puter authentication, so BEFORE writing worker ' + + 'code load the router guide and examples via puter_docs_get with path "Workers/router". ' + + 'Typical flow: fs_write_file the worker code to a path (e.g. "/me/workers/api.js"), then ' + + 'workers_create with that file_path. TO UPDATE a deployed worker, simply write the new code ' + + 'to the SAME file with fs_write_file — there is no separate update call; the worker serves ' + + 'the current contents of its associated file (propagation takes ~5-30s). Requires a Puter ' + + 'account with a verified email. Equivalent to PuterJS puter.workers.create(worker_name, file_path).', + inputSchema: { + type: 'object', + properties: { + worker_name: { + type: 'string', + description: 'Worker name (letters, digits, hyphens, underscores). Lowercased automatically.', + }, + file_path: { + type: 'string', + description: 'Path to the worker JS file in Puter (e.g. "/me/workers/api.js"). The file must define handlers on the global `router` object. Max 10MB. Writing to this same path later updates the deployed worker.', + }, + }, + required: ['worker_name', 'file_path'], + }, + async handler(puter, { worker_name, file_path }) { + return puter.workers.create(worker_name, file_path); + }, + }, + { + name: 'workers_list', + description: + 'List all serverless Workers deployed by the authenticated Puter user, including each ' + + "worker's name, public URL, and the source file it is deployed from (write to that file to " + + 'update the worker). Equivalent to PuterJS puter.workers.list().', + inputSchema: { type: 'object', properties: {} }, + async handler(puter) { + return puter.workers.list(); + }, + }, + { + name: 'workers_get', + description: + 'Get a single deployed Worker by name, including its public URL and the source file path it ' + + 'serves (write new code to that file with fs_write_file to update it). ' + + 'Equivalent to PuterJS puter.workers.get(worker_name).', + inputSchema: { + type: 'object', + properties: { + worker_name: { type: 'string', description: 'The worker name to look up.' }, + }, + required: ['worker_name'], + }, + async handler(puter, { worker_name }) { + return puter.workers.get(worker_name); + }, + }, + { + name: 'workers_exec', + description: + 'Call a deployed Puter Worker over HTTP as the authenticated user, automatically attaching ' + + "the Puter auth header so the worker can act on the caller's resources via `user.puter`. " + + 'Use this to invoke or test a worker endpoint. Equivalent to PuterJS puter.workers.exec(url, options).', + inputSchema: { + type: 'object', + properties: { + url: { + type: 'string', + description: 'Full worker URL including any path, e.g. "https://my-worker.puter.work/api/hello" (get the base URL from workers_get/workers_list).', + }, + method: { + type: 'string', + enum: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'], + default: 'GET', + }, + headers: { + type: 'object', + description: 'Optional request headers.', + additionalProperties: { type: 'string' }, + }, + body: { type: 'string', description: 'Optional request body (for POST/PUT/PATCH).' }, + }, + required: ['url'], + }, + async handler(puter, { url, method = 'GET', headers, body }) { + const init = { method }; + if (headers) init.headers = headers; + if (body != null && method !== 'GET' && method !== 'HEAD') init.body = body; + const resp = await puter.workers.exec(url, init); + const text = await resp.text(); + return { _meta: { status: resp.status, content_type: resp.headers.get('content-type') }, text }; + }, + }, + { + name: 'workers_delete', + description: + 'Delete (undeploy) a Puter Worker by name, stopping its execution and releasing its URL. Does ' + + "NOT delete the worker's source file in the filesystem. Equivalent to PuterJS puter.workers.delete(worker_name).", + inputSchema: { + type: 'object', + properties: { + worker_name: { type: 'string', description: 'The worker name to delete.' }, + }, + required: ['worker_name'], + }, + async handler(puter, { worker_name }) { + const ok = await puter.workers.delete(worker_name); + return { success: ok === true, deleted: worker_name }; + }, + }, + + // ----- puter.js documentation ------------------------------------------ + { + name: 'puter_docs_index', + description: + 'Load the index of Puter / puter.js documentation (from docs.puter.com/llms.txt): a list of ' + + 'every topic and its doc path, spanning the whole puter.js SDK — Workers (serverless ' + + 'functions), Hosting, FS, KV, AI (500+ models), Auth, and more. Call this FIRST to discover ' + + 'which doc to read, then fetch the page with puter_docs_get. Puter Workers and the tools in ' + + 'this server are designed to be used WITH puter.js and Puter authentication, so consult ' + + 'these docs before writing any worker or SDK code.', + inputSchema: { type: 'object', properties: {} }, + async handler() { + const text = await fetchDocText(DOCS_INDEX_URL); + return { _meta: { source: DOCS_INDEX_URL }, text }; + }, + }, + { + name: 'puter_docs_get', + description: + 'Fetch a specific Puter / puter.js documentation page as Markdown by the topic path listed in ' + + 'puter_docs_index. Examples: "Workers/router" (the Worker router guide + canonical examples — ' + + 'read this before writing a worker), "Workers/create", "AI/chat", "KV/set", "FS/write", ' + + '"Hosting/create". Use it to read the exact API and copy working examples before writing ' + + 'worker or puter.js code.', + inputSchema: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Doc topic path from the index, e.g. "Workers/router" or "AI/chat". A trailing "/index.md" is optional. Must be a docs.puter.com topic.', + }, + }, + required: ['path'], + }, + async handler(puter, { path }) { + const url = resolveDocUrl(path); + const text = await fetchDocText(url); + return { _meta: { source: url }, text }; + }, + }, ]; export const TOOL_MAP = new Map(TOOLS.map((t) => [t.name, t]));