# Host on Aerostack

> Build your own MCP server and deploy it to Cloudflare edge. No infrastructure to manage — just code, deploy, and add to your workspace.

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](/mcp/proxy-existing) 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.json
```

### `aerostack.json`

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

```json
{
  "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"
}
```

  **You do NOT need to declare tools in `aerostack.json`.** When you deploy, Aerostack automatically calls `tools/list` on your deployed worker, extracts all tool definitions, and stores them in the catalog. Your tool definitions in `src/index.ts` are the single source of truth.

### `aerostack.toml`

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:

```toml
name = "my-mcp-server"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[vars]
INTERNAL_API_KEY = ""
```

  **Important:** The first env var in `aerostack.toml` becomes the primary `auth_secret_key`. This tells the platform what credential name your MCP expects. For example, if your MCP needs a GitHub token, put `GITHUB_TOKEN = ""` first in `[vars]`.

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.

### `src/index.ts`

The MCP server entry point. This is a standard Cloudflare Worker that handles MCP protocol requests:

```typescript

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}`,
    }],
  };
});

```

---

## Build and Deploy

1. **Scaffold the project**

   ```bash
   aerostack mcp init my-mcp-server
   cd my-mcp-server
   npm install
   ```

1. **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).

1. **Test locally**

   ```bash
   npm run dev
   # -> MCP server running at http://localhost:8787
   ```

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

1. **Deploy to Cloudflare edge**

   ```bash
   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.
   

1. **Set secrets**

   ```bash
   aerostack secrets set my-mcp-server INTERNAL_API_KEY "sk-your-api-key"
   # -> Secret stored (AES-GCM encrypted).
   ```

1. **Add to your workspace**

   ```bash
   aerostack mcp install my-mcp-server --workspace my-workspace
   ```

   Your tools are now available:

   ```
   my-mcp-server__lookup_customer
   my-mcp-server__create_ticket
   ```

---

## What Happens on Deploy

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

```mermaid
flowchart TD
    A["aerostack deploy mcp"] --> B["Bundle + upload Worker todispatch namespace"]
    B --> C["Create/update mcp_servers row(slug, description, category, tags)"]
    C --> D["Sync config_schema fromaerostack.toml env vars"]
    D --> E["Fetch tools/list from Worker(2 retries with 1s + 3s delays)"]
    E --> F["Generate capability manifest(LLM or heuristic fallback)"]
    F --> G["Auto-resolve matching gap specs"]
```

### Auto-Generated Capability Manifest

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

```json
{
  "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.

### Hourly Tool Refresh

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)

---

## Release Checklist

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

| Requirement | Why | How |
|------------|-----|-----|
| **Good tool descriptions** | AI agents use descriptions to decide which tool to call | Write clear, specific `description` fields on every tool |
| **`aerostack.toml` with `[vars]`** | Powers the Docs tab auth section | List all required API keys/tokens as `[vars]` entries |
| **`description` in `aerostack.json`** | Shown in Hub search results | Keep it under 500 chars, explain what the MCP does |
| **`category`** | Hub filtering and discovery | Use standard categories: `developer-tools`, `communication`, `productivity`, `analytics`, `ai-ml`, `data`, `commerce`, `security` |
| **`tags`** | Fine-grained search | Up to 20 tags, lowercase, relevant keywords |
| **README.md** | Long-form docs, linked from `aerostack.json` | Include setup instructions, examples, and limitations |
| **`inputSchema` on tools** | Validation + auto-generated forms | Use JSON Schema with `required` fields and `description` on every property |

  **Pro tip:** The quality of your tool descriptions directly affects how well AI agents discover and use your MCP. Write descriptions as if explaining to a colleague what the tool does — not just "creates a ticket" but "creates a support ticket in the ticketing system with subject, body, and priority level".

---

## Convert 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.

```bash
aerostack mcp convert --package @notionhq/notion-mcp-server --deploy --slug notion-mcp
# -> Registered as notion-mcp
```

```bash
aerostack mcp convert --github https://github.com/user/my-mcp-server --deploy --slug my-mcp
```

```bash
aerostack mcp convert --dir ./my-local-mcp --slug my-mcp
aerostack deploy 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.)

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:

```yaml
# .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:

```bash
# 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`:

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

---

## Platform Bindings

Hosted MCP servers have access to the full Aerostack platform:

```typescript
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`:

```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](/mcp/proxy-existing) instead of hosting
- [Configure secrets](/mcp/secrets-security) for your hosted server
- [Add team members](/mcp/team-management) to your workspace
- [Back a skill with an edge function](/mcp/function-backed) for simpler single-tool use cases
