Host on Aerostack
Build a custom MCP server and deploy it to Cloudflare’s edge network. Your server gets a permanent HTTPS URL, runs globally with zero cold starts, and scales to zero when idle. No ngrok, no VPS, no Docker.
Hosted MCP servers are stored with hosted=1 and a worker_url pointing to your deployed Cloudflare Worker.
When to Host on Aerostack
- You want to build a custom integration (internal API, proprietary data source, business logic)
- You want zero infrastructure — no servers, no SSL, no scaling config
- You want to publish to the Hub for others to install
- You want native platform bindings (Database, Cache, Storage, AI, Vector Search)
If you already have an MCP server running on your own infrastructure, see Proxy Existing MCP instead.
Project Structure
An Aerostack-hosted MCP server follows this structure:
my-mcp-server/
src/
index.ts # MCP server entry point
aerostack.json # Aerostack MCP configuration
aerostack.toml # Cloudflare Worker configuration
package.json
tsconfig.jsonaerostack.json
This is the MCP server manifest. It tells Aerostack about your server’s tools, required secrets, and metadata:
{
"name": "my-mcp-server",
"slug": "my-mcp-server",
"version": "1.0.0",
"description": "A custom MCP server for internal APIs",
"tools": [
{
"name": "lookup_customer",
"description": "Look up a customer by email address",
"inputSchema": {
"type": "object",
"properties": {
"email": {
"type": "string",
"description": "Customer email address"
}
},
"required": ["email"]
}
},
{
"name": "create_ticket",
"description": "Create a support ticket for a customer",
"inputSchema": {
"type": "object",
"properties": {
"customer_email": {
"type": "string",
"description": "Customer email"
},
"subject": {
"type": "string",
"description": "Ticket subject"
},
"body": {
"type": "string",
"description": "Ticket description"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "critical"],
"description": "Ticket priority level"
}
},
"required": ["customer_email", "subject", "body"]
}
}
],
"secrets": ["INTERNAL_API_KEY"],
"visibility": "team"
}src/index.ts
The MCP server entry point. This is a standard Cloudflare Worker that handles MCP protocol requests:
import { McpServer } from '@aerostack/mcp';
const server = new McpServer({
name: 'my-mcp-server',
version: '1.0.0',
});
server.tool('lookup_customer', async ({ email }, env) => {
const response = await fetch(`https://api.internal.yourcompany.com/customers?email=${email}`, {
headers: { 'Authorization': `Bearer ${env.INTERNAL_API_KEY}` },
});
const customer = await response.json();
return {
content: [{
type: 'text',
text: JSON.stringify(customer, null, 2),
}],
};
});
server.tool('create_ticket', async ({ customer_email, subject, body, priority }, env) => {
const response = await fetch('https://api.internal.yourcompany.com/tickets', {
method: 'POST',
headers: {
'Authorization': `Bearer ${env.INTERNAL_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
customer_email,
subject,
body,
priority: priority || 'medium',
}),
});
const ticket = await response.json();
return {
content: [{
type: 'text',
text: `Ticket #${ticket.id} created: ${ticket.subject}`,
}],
};
});
export default server;Build and Deploy
Scaffold the project
aerostack mcp init my-mcp-server
cd my-mcp-server
npm installWrite your tool handlers
Edit src/index.ts with your tool logic. Each tool receives the input arguments and the Worker env object (for secrets and bindings).
Test locally
npm run dev
# -> MCP server running at http://localhost:8787You can test with any MCP client pointed at http://localhost:8787/sse.
Deploy to Cloudflare edge
aerostack deploy mcp
# -> Deploying my-mcp-server to Cloudflare edge...
# -> Worker URL: https://mcp-my-mcp-server.yourname.workers.dev
# -> MCP registered in Aerostack (hosted=1)Always use aerostack deploy mcp to deploy MCP servers. Do not use wrangler deploy directly — it will deploy the Worker but not register it in Aerostack.
Set secrets
aerostack secrets set my-mcp-server INTERNAL_API_KEY "sk-your-api-key"
# -> Secret stored (AES-GCM encrypted).Add to your workspace
aerostack mcp install my-mcp-server --workspace my-workspaceYour tools are now available:
my-mcp-server__lookup_customer
my-mcp-server__create_ticketConvert an Existing stdio MCP Server
If you have an MCP server that runs as a local stdio process (from npm, GitHub, or a local directory), you can convert it to a Cloudflare Worker and host it on Aerostack.
aerostack mcp convert --package @notionhq/notion-mcp-server --deploy --slug notion-mcp
# -> Analyzed tool definitions
# -> Generated Cloudflare Worker wrapper (HTTP SSE <-> stdio)
# -> Deployed to edge
# -> Registered as notion-mcpThe conversion process:
- Downloads and analyzes the MCP server source
- Extracts tool definitions and required environment variables
- Generates a Cloudflare Worker wrapper that translates HTTP SSE to stdio
- Flags incompatible Node.js APIs (file system, child processes, etc.)
Not all stdio MCP servers can be converted. Servers that depend on local file system access, child processes, or native Node.js modules will need modifications. The converter flags these issues.
Deploy via CI/CD
Automate deployments from GitHub Actions:
# .github/workflows/deploy-mcp.yml
name: Deploy MCP Server
on:
push:
branches: [main]
paths: ['src/**', 'aerostack.json']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx aerostack deploy mcp
env:
AEROSTACK_API_KEY: ${{ secrets.AEROSTACK_API_KEY }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }}Publish to the Hub
Once your MCP server is working, you can publish it for the community:
# Set visibility to public
aerostack mcp update my-mcp-server --visibility public
# Publish to Hub
aerostack mcp publish my-mcp-server
# -> Published to Hub at aerostack.dev/hub/yourname/my-mcp-serverInclude a README in your aerostack.json:
{
"readme": "README.md",
"tags": ["crm", "support", "internal-tools"],
"license": "MIT"
}Platform Bindings
Hosted MCP servers have access to the full Aerostack platform:
server.tool('query_database', async ({ sql }, env) => {
// Database query
const result = await env.DB.prepare(sql).all();
return { content: [{ type: 'text', text: JSON.stringify(result.results) }] };
});
server.tool('store_document', async ({ key, content }, env) => {
// Object storage
await env.R2.put(key, content);
return { content: [{ type: 'text', text: `Stored document at ${key}` }] };
});
server.tool('cache_lookup', async ({ key }, env) => {
// Cache lookup
const value = await env.KV.get(key);
return { content: [{ type: 'text', text: value || 'Not found' }] };
});
server.tool('summarize', async ({ text }, env) => {
// Workers AI
const result = await env.AI.run('@cf/meta/llama-3.1-8b-instruct', {
messages: [{ role: 'user', content: `Summarize: ${text}` }],
});
return { content: [{ type: 'text', text: result.response }] };
});Configure bindings in aerostack.toml:
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
[[r2_buckets]]
binding = "R2"
bucket_name = "my-bucket"
[[kv_namespaces]]
binding = "KV"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
[ai]
binding = "AI"Next Steps
- Proxy an existing MCP server instead of hosting
- Configure secrets for your hosted server
- Add team members to your workspace
- Back a skill with an edge function for simpler single-tool use cases