Skip to content

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.


  • 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.


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.json

This is the MCP server manifest. It tells Aerostack about your server’s metadata, required secrets, and publishing info:

{
"name": "my-mcp-server",
"slug": "my-mcp-server",
"version": "1.0.0",
"description": "A custom MCP server for internal APIs",
"category": "developer-tools",
"tags": ["crm", "support", "internal-tools"],
"secrets": ["INTERNAL_API_KEY"],
"visibility": "team"
}

This is the Cloudflare Worker configuration. Any [vars] you declare here become the config schema — Aerostack reads them to auto-generate the Docs tab auth section, so users know what secrets to provide when installing your MCP:

name = "my-mcp-server"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[vars]
INTERNAL_API_KEY = ""

When users install your MCP from the Hub, they see these env var names in the Docs tab and know exactly what secrets to configure.

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;

  1. Scaffold the project

    Terminal window
    aerostack mcp init my-mcp-server
    cd my-mcp-server
    npm install
  2. Write 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).

  3. Test locally

    Terminal window
    npm run dev
    # -> MCP server running at http://localhost:8787

    You can test with any MCP client pointed at http://localhost:8787/sse.

  4. Deploy to Cloudflare edge

    Terminal window
    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)
  5. Set secrets

    Terminal window
    aerostack secrets set my-mcp-server INTERNAL_API_KEY "sk-your-api-key"
    # -> Secret stored (AES-GCM encrypted).
  6. Add to your workspace

    Terminal window
    aerostack mcp install my-mcp-server --workspace my-workspace

    Your tools are now available:

    my-mcp-server__lookup_customer
    my-mcp-server__create_ticket

When you run aerostack deploy mcp, the platform does much more than just upload your Worker:

aerostack deploy mcp

Bundle + upload Worker to
dispatch namespace

Create/update mcp_servers row
(slug, description, category, tags)

Sync config_schema from
aerostack.toml env vars

Fetch tools/list from Worker
(2 retries with 1s + 3s delays)

Generate capability manifest
(LLM or heuristic fallback)

Auto-resolve matching gap specs

After tools are fetched, Aerostack generates a capability manifest — a structured summary that powers intelligent discovery:

{
"capabilities": ["crud", "search", "webhooks"],
"data_types": ["customers", "tickets", "support-cases"],
"auth_required": "secret_headers",
"triggers_available": ["ticket.created", "ticket.updated"],
"pairs_with": ["slack-mcp", "email-mcp"],
"best_for": ["customer support automation", "ticket triage"],
"not_suitable_for": ["real-time streaming", "file storage"]
}

This is generated using Workers AI (Llama 3.1 8B) with a heuristic fallback. You don’t need to write this manually — it’s derived from your tool names, descriptions, README, category, and tags.

Aerostack runs an hourly cron that re-fetches tools/list from all hosted MCPs. This means:

  • If you add a new tool and redeploy, it appears in the catalog within an hour (or immediately on deploy)
  • If your MCP becomes unreachable, it retries up to 5 times before pausing
  • Function-backed and first-party MCPs are excluded (they don’t need refresh)

Before publishing your MCP to the Hub, ensure your project includes:

RequirementWhyHow
Good tool descriptionsAI agents use descriptions to decide which tool to callWrite clear, specific description fields on every tool
aerostack.toml with [vars]Powers the Docs tab auth sectionList all required API keys/tokens as [vars] entries
description in aerostack.jsonShown in Hub search resultsKeep it under 500 chars, explain what the MCP does
categoryHub filtering and discoveryUse standard categories: developer-tools, communication, productivity, analytics, ai-ml, data, commerce, security
tagsFine-grained searchUp to 20 tags, lowercase, relevant keywords
README.mdLong-form docs, linked from aerostack.jsonInclude setup instructions, examples, and limitations
inputSchema on toolsValidation + auto-generated formsUse JSON Schema with required fields and description on every property

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.

Terminal window
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-mcp

The conversion process:

  1. Downloads and analyzes the MCP server source
  2. Extracts tool definitions and required environment variables
  3. Generates a Cloudflare Worker wrapper that translates HTTP SSE to stdio
  4. Flags incompatible Node.js APIs (file system, child processes, etc.)

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 }}

Once your MCP server is working, you can publish it for the community:

Terminal window
# 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-server

Include a README in your aerostack.json:

{
"readme": "README.md",
"tags": ["crm", "support", "internal-tools"],
"license": "MIT"
}

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"