├── .assetsignore ├── .dev.vars.example ├── .gitignore ├── README.md ├── TODO.md ├── curl.ts ├── default-proxy.yaml ├── ideas.md ├── index.html ├── main.ts ├── openapi.json ├── package.json └── wrangler.jsonc /.assetsignore: -------------------------------------------------------------------------------- 1 | .git 2 | .wrangler 3 | node_modules 4 | .dev.vars 5 | package-lock.json -------------------------------------------------------------------------------- /.dev.vars.example: -------------------------------------------------------------------------------- 1 | CREDENTIALS= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .git 2 | .wrangler 3 | node_modules 4 | .dev.vars 5 | package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [curl mcp](https://curlmcp.com) - the last MCP you'll need 2 | 3 | [![janwilmake/curlmcp context](https://badge.forgithub.com/janwilmake/curlmcp)](https://uithub.com/janwilmake/curlmcp?tab=readme-ov-file) [![Thread](https://badge.xymake.com/NathanWilbanks_/status/1898169822573175179?label=Inspiration_SLOP&a)](https://xymake.com/NathanWilbanks_/status/1898169822573175179) [![Thread](https://badge.xymake.com/janwilmake/status/1903372996128960928?label=Inspiration_Sam)](https://xymake.com/janwilmake/status/1903372996128960928) 4 | 5 | > [!IMPORTANT] 6 | > WORK IN PROGRESS 7 | 8 | Requirements: 9 | 10 | - Exposes simple REST API (with OpenAPI) as well as Remote MCP 11 | - Easy to use through any MCP client, through API, and through browsers. 12 | - X & GitHub OAuth 13 | - Stripe credit deposit 14 | - Contextual Instructions 15 | - Markdown Transformation Proxy for popular websites such as X and GitHub 16 | - Capped free use (per-hour ratelimit), pay as you go after hitting cap. 17 | - Shareable instruction templates 18 | 19 | # My principles for making the LLM actually work well with tons of tools: 20 | 21 | 1. As the LLM knows popular websites, instruct it to simply use the web like normal. 22 | 2. Under water, ensure every input is somehow routed to the right substitute website(s). 23 | 3. Ensure every response is markdown and contains very few tokens, ideally less than 1000! This ensures we can do many steps. 24 | 4. Ensure the dead ends guide the LLM back on track. 25 | 5. Ensure every step in a multi-step process contains instructions about what to do next. 26 | 6. Ensure the path the LLM visit is the same as the path the user or crawler visits. Respond well on accept header and other information to distinguish. 27 | 28 | How should product builders of today become ready to allow for this? 29 | 30 | 1. Most APIs use POST, but GET is easier to be instructed about, as it can be done in markdown. Let's promote making APIs GET and promote super easy to understand URL structures with minimal token length. 31 | 2. Ensure to use OpenAPI to show the possible endpoints and routing. Your API should be the first-class citizen, not your website. 32 | 3. Ensure to make your openapi explorable by either putting it right on the root at `/openapi.json`, or by putting redirecting to it from `/.well-known/openapi` if that's not possible. 33 | 4. Ensure all your pages that are exposed as text/html also expose a non-html variant (preferably markdown, or yaml if structured data can also be useful) that is under 1000 tokens with the same/similar functionality. 34 | 5. Hitting errors in your API should always guide the agent back on track, just like we do with humans. Try buildling these UX pathways on the API level! 35 | 36 | Is it somehow possible to provide this as a middleware to APIs? For sure! The one tool to rule them all is curl (or fetch), and it could be made safe in the following way: 37 | 38 | - Ensure to route away from human-first websites to ai-optimised websites. 39 | - Ensure to truncate the response to never be above a certain limit 40 | - Ensure to prefer accepting markdown 41 | 42 | # Usage 43 | 44 | ## MCP Usage: 45 | 46 | Install it into your MCP Client by adding the following to your config: 47 | 48 | ```json 49 | { 50 | "mcpServers": { 51 | "curlmcp": { 52 | "command": "npx", 53 | "args": ["mcp-remote", "https://curlmcp.com/sse"] 54 | } 55 | } 56 | } 57 | ``` 58 | 59 | ## Browser Usage 60 | 61 | The curlmcp api is easy to use from the browser too. Authentication is automatically handled. 62 | 63 | ## CLI Usage 64 | 65 | You can simply use curl yourself, or to have the curl mcp proxy: 66 | 67 | - use `curl -c cookies.txt https://curlmcp.com/login` to login and store cookies 68 | - use `curl -b cookies.txt https://curlmcp.com/curl/{your-request}` 69 | 70 | ## API Usage: `/curl/{url}` Endpoint 71 | 72 | The `/curl/{url}` endpoint allows you to send HTTP requests to any URL, mimicking the behavior of the `curl` command-line tool. It supports long-form query parameters to specify request details, such as the HTTP method, headers, and data. 73 | 74 | Certain urls are configured to be proxied based on your configured template (defaults to [default-proxy.yaml](default-proxy.yaml)) 75 | 76 | ### API Specification 77 | 78 | ``` 79 | GET /curl/{url}?request={method}&header={header}&data={data}&... 80 | ``` 81 | 82 | NB: {url} uses the `https` protocol by default. 83 | 84 | #### Supported Query Parameters 85 | 86 | | Parameter | Type | Description | Example | 87 | | ---------------- | ---------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------- | 88 | | `request` | string | Specifies the HTTP method. Valid values: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`. | `request=POST` | 89 | | `header` | array of strings | Adds custom HTTP headers. Repeat for multiple headers. | `header=Content-Type:application/json` | 90 | | `data` | array of strings | Sends data in the request body (POST) or query string (with `get=true`). Repeat for multiple data pairs. | `data=key=value` | 91 | | `data-urlencode` | array of strings | Sends URL-encoded data in the request. | `data-urlencode=comment=this%20is%20awesome` | 92 | | `get` | boolean | Forces data to be sent as a GET request query string. | `get=true` | 93 | | `include` | boolean | Includes response headers in the output. | `include=true` | 94 | | `head` | boolean | Sends a HEAD request. | `head=true` | 95 | | `user` | string | Specifies credentials for authentication (format: `username:password`). | `user=user:pass` | 96 | | `location` | boolean | Follows HTTP redirects. | `location=true` | 97 | | `verbose` | boolean | Enables verbose output for debugging. | `verbose=true` | 98 | | `access_token` | string | Injects an OAuth token for X or GitHub authentication. | `access_token=xyz` | 99 | | `instructions` | string | Specifies contextual instructions for the request. | `instructions=transform_response_to_markdown` | 100 | 101 | # Links 102 | 103 | - Previous attempt (curl api): https://github.com/janwilmake/curlapi 104 | - Previous attempt (fetch mcp): https://github.com/janwilmake/fetch-mcp 105 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # 1) Make authless curl work 2 | 3 | - ✅ Create `/openapi.json` 4 | - ✅ Build out curl and test the CURL itself protected with auth 5 | - ✅ Setup remote mcp (`/sse`) hosted in cloudflare behind my own authwall (Take example from https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-server, but without the auth, as I provide that myself) 6 | - Figure out how to provide the tool params to `AgentMcp` from the package 7 | - Test this from claude and potential mcp playground (without oauth should be fine, auth is optional) 8 | 9 | # 2) Add auth and payments 10 | 11 | - Add my own X oauth with Stripe payments and dashboard where api keys can be created (First make that work at https://github.com/janwilmake/xymake.oauth-stripe-template). 12 | - Perform downstream payments via my own personal API tokens. 13 | 14 | # 3) Add needed proxies 15 | 16 | - Add firecrawl or similar as fallback scraper 17 | 18 | # 4) openapi-to-mcp 19 | 20 | Abstract the openapi-to-mcp into a middleware that can be passed the openapi. 21 | -------------------------------------------------------------------------------- /curl.ts: -------------------------------------------------------------------------------- 1 | /** CURL request->response. Pretty much 100% implemented by Claude*/ 2 | export const curl = async (request: Request) => { 3 | const url = new URL(request.url); 4 | const pathname = url.pathname; 5 | 6 | // Extract the target URL from the /curl/{url} path 7 | if (!pathname.startsWith("/curl/")) { 8 | return new Response("Invalid curl endpoint", { status: 400 }); 9 | } 10 | 11 | // Extract the target URL (everything after /curl/) 12 | let targetUrl = pathname.substring(6); // Remove "/curl/" 13 | 14 | // Default to https protocol if not specified 15 | if (!targetUrl.startsWith("http://") && !targetUrl.startsWith("https://")) { 16 | targetUrl = "https://" + targetUrl; 17 | } 18 | 19 | console.log({ targetUrl }); 20 | 21 | // Parse query parameters 22 | const params = new URLSearchParams(url.search); 23 | 24 | // Build request options 25 | const options = { 26 | method: params.get("request") || "GET", 27 | headers: {} as { [key: string]: string }, 28 | redirect: params.get("location") === "true" ? "follow" : "manual", 29 | body: undefined as string | undefined | FormData, 30 | }; 31 | 32 | // Process headers 33 | params.getAll("header").forEach((header) => { 34 | const [name, value] = header.split(":", 2); 35 | if (name && value) { 36 | options.headers[name.trim()] = value.trim(); 37 | } 38 | }); 39 | 40 | // Handle authentication 41 | const user = params.get("user"); 42 | if (user) { 43 | const [username, password] = user.split(":", 2); 44 | const authHeader = "Basic " + btoa(username + ":" + (password || "")); 45 | options.headers.Authorization = authHeader; 46 | } 47 | 48 | // OAuth token handling 49 | const accessToken = params.get("access_token"); 50 | if (accessToken) { 51 | options.headers.Authorization = `Bearer ${accessToken}`; 52 | } 53 | 54 | // Handle request body data 55 | if (params.has("data") || params.has("data-urlencode")) { 56 | const formData = new FormData(); 57 | 58 | // Process regular data 59 | params.getAll("data").forEach((data) => { 60 | const [key, value] = data.split("=", 2); 61 | if (key) formData.append(key, value || ""); 62 | }); 63 | 64 | // Process URL-encoded data 65 | params.getAll("data-urlencode").forEach((data) => { 66 | const [key, value] = data.split("=", 2); 67 | if (key) formData.append(key, decodeURIComponent(value || "")); 68 | }); 69 | 70 | // If GET is forced or if it's a GET request 71 | if (params.get("get") === "true" || options.method === "GET") { 72 | // Append form data to URL as query string 73 | const searchParams = new URLSearchParams(); 74 | formData.forEach((value, key) => { 75 | searchParams.append(key, value.toString()); 76 | }); 77 | 78 | // Update target URL with query parameters 79 | const urlObj = new URL(targetUrl); 80 | const existingParams = new URLSearchParams(urlObj.search); 81 | searchParams.forEach((value, key) => { 82 | existingParams.append(key, value); 83 | }); 84 | urlObj.search = existingParams.toString(); 85 | targetUrl = urlObj.toString(); 86 | } else { 87 | // For non-GET requests, set the body 88 | options.body = formData; 89 | } 90 | } 91 | 92 | // Force HEAD method if specified 93 | if (params.get("head") === "true") { 94 | options.method = "HEAD"; 95 | } 96 | 97 | // Check for proxy configuration based on URL 98 | const proxyConfig = getProxyConfig(targetUrl); 99 | if (proxyConfig) { 100 | // Apply proxy transformations 101 | targetUrl = proxyConfig.targetUrl || targetUrl; 102 | 103 | // Apply additional headers from proxy config 104 | if (proxyConfig.headers) { 105 | Object.entries(proxyConfig.headers).forEach(([name, value]) => { 106 | options.headers[name] = value as string; 107 | }); 108 | } 109 | } 110 | 111 | try { 112 | console.log({ targetUrl, options }); 113 | // Make the actual request 114 | const response = await fetch(targetUrl, options as RequestInit); 115 | 116 | // Prepare response 117 | let responseBody: any; 118 | const contentType = response.headers.get("Content-Type") || ""; 119 | 120 | // Limit response size to avoid excessive token usage 121 | const maxResponseSize = 100 * 1024; // 100KB limit 122 | 123 | responseBody = await response.text(); 124 | 125 | // Truncate if too large 126 | if (responseBody.length > maxResponseSize) { 127 | responseBody = 128 | responseBody.substring(0, maxResponseSize) + 129 | "\n\n... Response truncated due to size limits. Full size: " + 130 | responseBody.length + 131 | " bytes"; 132 | } 133 | 134 | const responseHeaders: HeadersInit = { "Content-Type": contentType }; 135 | 136 | // Include original headers if requested 137 | if (params.get("include") === "true") { 138 | responseBody = 139 | formatResponseHeaders(response.headers) + "\n\n" + responseBody; 140 | } 141 | 142 | return new Response(responseBody, { 143 | headers: responseHeaders, 144 | status: response.status, 145 | }); 146 | } catch (error) { 147 | return new Response(`Error making request: ${error.message}`, { 148 | status: 500, 149 | }); 150 | } 151 | }; 152 | 153 | // Helper functions 154 | function getProxyConfig(url: string): any { 155 | // This would load from your default-proxy.yaml or custom templates 156 | // For now, returning a simple dummy configuration 157 | const urlObj = new URL(url); 158 | 159 | // Example proxy configurations 160 | const proxyConfigs: Record = { 161 | "x.com": { 162 | targetUrl: url.replace("x.com", "xymake.com"), 163 | headers: { 164 | Accept: "text/markdown", 165 | }, 166 | }, 167 | "github.com": { 168 | targetUrl: url.replace("github.com", "uithub.com"), 169 | headers: { 170 | Accept: "text/markdown", 171 | }, 172 | }, 173 | }; 174 | 175 | // Check if we have a proxy config for this domain 176 | return proxyConfigs[urlObj.hostname]; 177 | } 178 | 179 | function formatResponseHeaders(headers: Headers): string { 180 | let result = "### Response Headers\n\n```\n"; 181 | headers.forEach((value, key) => { 182 | result += `${key}: ${value}\n`; 183 | }); 184 | result += "```"; 185 | return result; 186 | } 187 | -------------------------------------------------------------------------------- /default-proxy.yaml: -------------------------------------------------------------------------------- 1 | instructions: | 2 | For context about the user, check x.com/{xUsername} and github.com/{githubOwner} 3 | When unsure about how a website works, use openapisearch.com/{domainOrQuery} 4 | 5 | proxies: 6 | x.com: xymake.com 7 | github.com: uithub.com 8 | *: reader.llmtext.com/md/* 9 | # *: firecrawl.com 10 | # google.com: googllm.com (coming soon) 11 | -------------------------------------------------------------------------------- /ideas.md: -------------------------------------------------------------------------------- 1 | # MCP interacting with GitHub, X, and API specs. 2 | 3 | Work towards making this work perfectly. In the end, instructions aren't even needed, as I can replace it programatically. The important thing is that the LLM gets guided properly in the responses and knows and can tell the user what's actually happening and tells the user how he done it, so people end up actually sharing the link containing the og:image. 4 | 5 | After this works smoothly, #1 to figure out is auth, to track usage of this MCP. GitHub oauth is best! 6 | 7 | Let's work towards this as an ultimate MCP! 8 | 9 | The philosophy behind slop can be embedded as additional params in the openapi; `x-workflow-operations: string[]`. 10 | 11 | This could ensure that it shows a summary of how to use these operation(s) using GET, such that it becomes a workflow. 12 | 13 | # Conversation sessionId 14 | 15 | Ensure a sessionID of the conversation is sent, which we can use to estimate tokensize. When tokensize becomes excessively large, ensure instructions are added that summarize the obtained context so far and a link to start a new conversation. 16 | 17 | # safari ext for browsing in markdown and rendering it as markdown too 18 | 19 | https://claude.ai/share/952bfd94-ca94-4da1-8682-e8bdc9cf5d9a 20 | 21 | the goal would be to see if a website is fully LLM ready by clicking around and see if all is reachable and easy to understand. 22 | 23 | # other name 24 | 25 | mcp-mcp 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | curl MCP - The last MCP you'll need 8 | 91 | 92 | 93 | 94 |
95 |

The last MCP you'll need

96 |

curl MCP: A powerful, lightweight, and reliable MCP built on HTTP standards.

97 | Get curl MCP on GitHub 98 |
99 | 100 |
101 |

What People Are Saying

102 |
103 |
104 | 107 |
108 |
109 | 112 |
113 |
114 | 117 |
118 |
119 | 120 |
121 | 122 | 123 | 124 |
125 |
126 |

Pricing

127 |

Freemium model: Use curl MCP for free. Easily add balance to your account to pay for 128 | premium downstream markdown-proxies and unlock advanced functionality.

129 | Learn More About Pricing 130 |
131 |
132 | 133 |
134 |
135 |

Security

136 |

OAuth 2.1 compliant with PKCE scope support, ensuring secure authentication and authorization for your 137 | integrations.

138 | Explore Security Features 139 |
140 |
141 | 142 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { McpAgent } from "agents/mcp"; 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 3 | import { curl } from "./curl"; 4 | import openapi from "./openapi.json"; 5 | 6 | type Env = { 7 | CREDENTIALS: string; 8 | MCP_OBJECT: DurableObjectNamespace; 9 | }; 10 | 11 | type OpenapiOperation = any; 12 | 13 | /** 14 | TODO: I've done this before, let's find that and keep it simple (not merging the different rest params)*/ 15 | const operationToSchema = (operation: OpenapiOperation) => { 16 | // input params going into the tool 17 | return { 18 | type: "object", 19 | additionalProperties: false, 20 | properties: { 21 | headers: { type: "object" }, 22 | query: { type: "object" }, 23 | body: { type: "object" }, 24 | }, 25 | }; 26 | }; 27 | 28 | export class MyMCP extends McpAgent { 29 | server = new McpServer( 30 | { 31 | name: openapi.info.title, 32 | version: openapi.info.version, 33 | }, 34 | { 35 | instructions: openapi.info.description, 36 | capabilities: { 37 | tools: { 38 | // Is this where the schema goes of input params? For now, can do it manually, but for OpenAPI to MCP, we can do this. 39 | // operationToSchema(openapi.paths["/curl/{url}"].get); 40 | curl: {}, 41 | }, 42 | }, 43 | }, 44 | ); 45 | 46 | async init() { 47 | // this can be done iterated over the operations 48 | 49 | this.server.tool( 50 | openapi.paths["/curl/{url}"].get.operationId, 51 | // this must be zod-defined so the next one is typed. is this what's provided to the tool, or can i provide it upthere in McpServer.capabilities? 52 | {} as any, 53 | // Execute the tool 54 | async ({ headers, query, body, path }, extra) => { 55 | // Build the request from the tool input 56 | const request = new Request( 57 | `http://dummy-url${path}${ 58 | query ? `?` + new URLSearchParams(query).toString() : "" 59 | }`, 60 | { headers, body: body ? JSON.stringify(body) : undefined }, 61 | ); 62 | // does this contain header info? 63 | // extra.authInfo 64 | const result = await curl(request); 65 | 66 | if (!result.ok) { 67 | return { 68 | isError: true, 69 | content: [ 70 | { 71 | type: "text", 72 | text: `${result.status} error - ${await result.text()}`, 73 | }, 74 | ], 75 | }; 76 | } 77 | 78 | // 20X response assumed to have text. However, I could also parse the other types here based on the response content-type 79 | const contentType = result.headers.get("content-type"); 80 | if (!contentType || contentType.startsWith("text/")) { 81 | try { 82 | const text = await result.text(); 83 | return { content: [{ type: "text", text }] }; 84 | } catch (e) { 85 | return { 86 | content: [{ type: "text", text: `Error 500 - ${e.message}` }], 87 | isError: true, 88 | }; 89 | } 90 | } 91 | 92 | // TODO: Parse other types 93 | return { 94 | content: [ 95 | { 96 | type: "text", 97 | text: `Error 500 - Type not supported yet. Tool returned ${contentType}`, 98 | }, 99 | ], 100 | isError: true, 101 | }; 102 | }, 103 | ); 104 | } 105 | } 106 | 107 | // Export the OAuth handler as the default 108 | export default { 109 | fetch: (request: Request, env: Env, ctx: ExecutionContext) => { 110 | const url = new URL(request.url); 111 | 112 | // TODO: Integrate oauth2 server 113 | 114 | const authorization = request.headers 115 | .get("authorization") 116 | ?.slice("Basic ".length); 117 | 118 | if (!authorization || atob(authorization) !== env.CREDENTIALS) { 119 | return new Response("Unauthorized", { 120 | status: 401, 121 | headers: { "WWW-Authenticate": 'Basic realm="Protected"' }, 122 | }); 123 | } 124 | 125 | if (url.pathname.startsWith("/curl/")) { 126 | // regular rest-based execution of the endpoint 127 | return curl(request); 128 | } 129 | 130 | if (url.pathname === "/sse") { 131 | // Serve the MCP 132 | return MyMCP.mount("/sse").fetch( 133 | request, 134 | { MCP_OBJECT: env.MCP_OBJECT }, 135 | ctx, 136 | ); 137 | } 138 | }, 139 | }; 140 | -------------------------------------------------------------------------------- /openapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.3", 3 | "info": { 4 | "title": "curl mcp API", 5 | "description": "API for curl mcp - the last MCP you'll need. Allows making HTTP requests to any URL through a simple REST API, mimicking the behavior of the curl command-line tool.", 6 | "version": "1.0.0", 7 | "contact": { 8 | "name": "Jan Wilmake", 9 | "url": "https://curlmcp.com" 10 | } 11 | }, 12 | "servers": [ 13 | { 14 | "url": "https://curlmcp.com", 15 | "description": "Production server" 16 | } 17 | ], 18 | "paths": { 19 | "/curl/{url}": { 20 | "get": { 21 | "summary": "Make a curl request to any URL", 22 | "description": "Sends an HTTP request to the specified URL with the given parameters, mimicking the behavior of the curl command-line tool.", 23 | "operationId": "curlRequest", 24 | "parameters": [ 25 | { 26 | "name": "url", 27 | "in": "path", 28 | "description": "The URL to send the request to (defaults to https protocol)", 29 | "required": true, 30 | "schema": { 31 | "type": "string" 32 | } 33 | }, 34 | { 35 | "name": "request", 36 | "in": "query", 37 | "description": "Specifies the HTTP method. Valid values: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS.", 38 | "required": false, 39 | "schema": { 40 | "type": "string", 41 | "enum": [ 42 | "GET", 43 | "POST", 44 | "PUT", 45 | "DELETE", 46 | "PATCH", 47 | "HEAD", 48 | "OPTIONS" 49 | ], 50 | "default": "GET" 51 | } 52 | }, 53 | { 54 | "name": "header", 55 | "in": "query", 56 | "description": "Adds custom HTTP headers. Repeat for multiple headers.", 57 | "required": false, 58 | "schema": { 59 | "type": "array", 60 | "items": { 61 | "type": "string" 62 | } 63 | }, 64 | "style": "form", 65 | "explode": true, 66 | "example": "Content-Type:application/json" 67 | }, 68 | { 69 | "name": "data", 70 | "in": "query", 71 | "description": "Sends data in the request body (POST) or query string (with get=true). Repeat for multiple data pairs.", 72 | "required": false, 73 | "schema": { 74 | "type": "array", 75 | "items": { 76 | "type": "string" 77 | } 78 | }, 79 | "style": "form", 80 | "explode": true, 81 | "example": "key=value" 82 | }, 83 | { 84 | "name": "data-urlencode", 85 | "in": "query", 86 | "description": "Sends URL-encoded data in the request.", 87 | "required": false, 88 | "schema": { 89 | "type": "array", 90 | "items": { 91 | "type": "string" 92 | } 93 | }, 94 | "style": "form", 95 | "explode": true, 96 | "example": "comment=this%20is%20awesome" 97 | }, 98 | { 99 | "name": "get", 100 | "in": "query", 101 | "description": "Forces data to be sent as a GET request query string.", 102 | "required": false, 103 | "schema": { 104 | "type": "boolean", 105 | "default": false 106 | } 107 | }, 108 | { 109 | "name": "include", 110 | "in": "query", 111 | "description": "Includes response headers in the output.", 112 | "required": false, 113 | "schema": { 114 | "type": "boolean", 115 | "default": false 116 | } 117 | }, 118 | { 119 | "name": "head", 120 | "in": "query", 121 | "description": "Sends a HEAD request.", 122 | "required": false, 123 | "schema": { 124 | "type": "boolean", 125 | "default": false 126 | } 127 | }, 128 | { 129 | "name": "user", 130 | "in": "query", 131 | "description": "Specifies credentials for authentication (format: username:password).", 132 | "required": false, 133 | "schema": { 134 | "type": "string" 135 | }, 136 | "example": "user:pass" 137 | }, 138 | { 139 | "name": "location", 140 | "in": "query", 141 | "description": "Follows HTTP redirects.", 142 | "required": false, 143 | "schema": { 144 | "type": "boolean", 145 | "default": true 146 | } 147 | }, 148 | { 149 | "name": "verbose", 150 | "in": "query", 151 | "description": "Enables verbose output for debugging.", 152 | "required": false, 153 | "schema": { 154 | "type": "boolean", 155 | "default": false 156 | } 157 | }, 158 | { 159 | "name": "access_token", 160 | "in": "query", 161 | "description": "Injects an OAuth token for X or GitHub authentication.", 162 | "required": false, 163 | "schema": { 164 | "type": "string" 165 | } 166 | }, 167 | { 168 | "name": "transform", 169 | "in": "query", 170 | "description": "Applies transformations to the response (e.g., markdown for X/GitHub compatibility).", 171 | "required": false, 172 | "schema": { 173 | "type": "string", 174 | "enum": ["markdown"] 175 | } 176 | }, 177 | { 178 | "name": "instructions", 179 | "in": "query", 180 | "description": "Specifies contextual instructions for the request.", 181 | "required": false, 182 | "schema": { 183 | "type": "string" 184 | }, 185 | "example": "transform_response_to_markdown" 186 | }, 187 | { 188 | "name": "template_id", 189 | "in": "query", 190 | "description": "Applies a predefined template of parameters.", 191 | "required": false, 192 | "schema": { 193 | "type": "string" 194 | } 195 | } 196 | ], 197 | "responses": { 198 | "200": { 199 | "description": "Successful response", 200 | "content": { 201 | "application/json": { 202 | "schema": { 203 | "type": "object", 204 | "properties": { 205 | "status": { 206 | "type": "integer", 207 | "description": "HTTP status code of the response" 208 | }, 209 | "headers": { 210 | "type": "object", 211 | "description": "Headers of the response (included when include=true)" 212 | }, 213 | "body": { 214 | "type": "string", 215 | "description": "Body of the response" 216 | } 217 | } 218 | } 219 | }, 220 | "text/markdown": { 221 | "schema": { 222 | "type": "string", 223 | "description": "Transformed markdown response (when transform=markdown)" 224 | } 225 | } 226 | } 227 | }, 228 | "400": { 229 | "description": "Bad request", 230 | "content": { 231 | "application/json": { 232 | "schema": { 233 | "type": "object", 234 | "properties": { 235 | "error": { 236 | "type": "string", 237 | "description": "Error message" 238 | } 239 | } 240 | } 241 | } 242 | } 243 | }, 244 | "401": { 245 | "description": "Unauthorized", 246 | "content": { 247 | "application/json": { 248 | "schema": { 249 | "type": "object", 250 | "properties": { 251 | "error": { 252 | "type": "string", 253 | "description": "Error message" 254 | } 255 | } 256 | } 257 | } 258 | } 259 | }, 260 | "403": { 261 | "description": "Forbidden", 262 | "content": { 263 | "application/json": { 264 | "schema": { 265 | "type": "object", 266 | "properties": { 267 | "error": { 268 | "type": "string", 269 | "description": "Error message" 270 | } 271 | } 272 | } 273 | } 274 | } 275 | }, 276 | "429": { 277 | "description": "Too many requests (rate limit exceeded)", 278 | "content": { 279 | "application/json": { 280 | "schema": { 281 | "type": "object", 282 | "properties": { 283 | "error": { 284 | "type": "string", 285 | "description": "Error message" 286 | }, 287 | "rate_limit_reset": { 288 | "type": "integer", 289 | "description": "Unix timestamp when the rate limit will reset" 290 | } 291 | } 292 | } 293 | } 294 | } 295 | }, 296 | "500": { 297 | "description": "Internal server error", 298 | "content": { 299 | "application/json": { 300 | "schema": { 301 | "type": "object", 302 | "properties": { 303 | "error": { 304 | "type": "string", 305 | "description": "Error message" 306 | } 307 | } 308 | } 309 | } 310 | } 311 | } 312 | }, 313 | "security": [ 314 | { 315 | "oauth2": [] 316 | } 317 | ] 318 | } 319 | }, 320 | "/login": { 321 | "get": { 322 | "summary": "Login to curl mcp", 323 | "description": "Login page for curl mcp. Can be used with browser cookies for authentication.", 324 | "operationId": "login", 325 | "responses": { 326 | "200": { 327 | "description": "Login page" 328 | } 329 | } 330 | } 331 | } 332 | }, 333 | "components": { 334 | "securitySchemes": { 335 | "oauth2": { 336 | "type": "oauth2", 337 | "flows": { 338 | "authorizationCode": { 339 | "authorizationUrl": "https://curlmcp.com/authorize", 340 | "tokenUrl": "https://curlmcp.com/callback", 341 | "scopes": { 342 | "curl": "Access to make curl requests", 343 | "read": "Read access to templates and history", 344 | "write": "Write access to templates" 345 | } 346 | } 347 | } 348 | } 349 | } 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "workers-mcp": "^0.1.0-3", 4 | "@modelcontextprotocol/sdk": "^1.7.0", 5 | "agents": "^0.0.53" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /wrangler.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "name": "curlmcp", 3 | "compatibility_date": "2025-04-19", 4 | "assets": { 5 | "directory": "./" 6 | }, 7 | "main": "./main.ts", 8 | "routes": [ 9 | { "pattern": "curlmcp.com", "custom_domain": true }, 10 | { "pattern": "www.curlmcp.com", "custom_domain": true } 11 | ], 12 | "dev": { "port": 3000 }, 13 | "compatibility_flags": ["nodejs_compat"], 14 | "migrations": [ 15 | { 16 | "new_sqlite_classes": ["MyMCP"], 17 | "tag": "v1" 18 | } 19 | ], 20 | "durable_objects": { 21 | "bindings": [ 22 | { 23 | "class_name": "MyMCP", 24 | "name": "MCP_OBJECT" 25 | } 26 | ] 27 | } 28 | } 29 | --------------------------------------------------------------------------------