# Astro Webmcp

> Astro integration that exposes your site content via WebMCP for AI agents

- **Type:** MCP server
- **Install:** `agentstack add mcp-fabricioctelles-astro-webmcp`
- **Verified:** Pending review
- **Seller:** [fabricioctelles](https://agentstack.voostack.com/s/fabricioctelles)
- **Installs:** 0
- **Category:** [Integrations](https://agentstack.voostack.com/c/integrations)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [fabricioctelles](https://github.com/fabricioctelles)
- **Source:** https://github.com/fabricioctelles/astro-webmcp
- **Website:** https://www.npmjs.com/package/astro-webmcp

## Install

```sh
agentstack add mcp-fabricioctelles-astro-webmcp
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# astro-webmcp

[](https://www.npmjs.com/package/astro-webmcp)
[](https://www.npmjs.com/package/astro-webmcp)
[](https://astro.build)
[](https://developer.chrome.com/docs/ai/webmcp)
[](./LICENSE)
[](https://www.typescriptlang.org/)

> 🤖 Make your Astro site AI-agent ready in one line of code.

Astro integration that automatically exposes your site's content via [WebMCP](https://developer.chrome.com/docs/ai/webmcp) — allowing AI agents to discover, search, and navigate your content directly in the browser.

## What is WebMCP?

WebMCP is a proposed web standard by Chrome that lets websites declare structured "tools" for AI agents. Instead of an agent visually interpreting each page element, the site explicitly declares what can be done — search articles, navigate to sections, get page metadata.

- **Spec:** https://webmachinelearning.github.io/webmcp/
- **Chrome docs:** https://developer.chrome.com/docs/ai/webmcp
- **GitHub:** https://github.com/webmachinelearning/webmcp

## Installation

```bash
npm install astro-webmcp
```

## Basic usage

```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import webmcp from 'astro-webmcp';

export default defineConfig({
  integrations: [webmcp()],
});
```

That's it. All your site content is now exposed via WebMCP automatically.

## Configuration options

```js
webmcp({
  collections: ['blog', 'docs'], // filter which collections to expose (default: all)

  // Custom tools — expose your own domain-specific functionality
  customTools: [
    {
      name: 'search_products',
      description: 'Search the product catalog by name or keyword.',
      inputSchema: {
        type: 'object',
        properties: {
          query: { type: 'string', description: 'Search term' },
        },
        required: ['query'],
      },
      executeBody: `return fetch('/api/search?q=' + encodeURIComponent(params.query))
        .then(r => r.json())
        .then(d => safeOutput(d));`,
      annotations: { readOnlyHint: true, untrustedContentHint: true },
    },
  ],

  // Search backend for search_content tool (default: 'manifest')
  search: {
    backend: 'pagefind',       // 'manifest' | 'pagefind' | 'orama'
    pagefindBundlePath: '/pagefind/',
    // oramaIndexUrl: '/search-index.json',  // required for 'orama'
  },

  security: {
    exposedTo: [],          // origins allowed cross-origin access (default: none)
    maxOutputLength: 1500,  // max chars per tool output (default: 1500)
    sanitizeOutputs: true,  // strip prompt injection patterns (default: true)
  },
})
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `collections` | `string[]` | `undefined` (all) | List of collections to include in the manifest |
| `customTools` | `CustomTool[]` | `[]` | Domain-specific tools registered alongside built-in ones |
| `search.backend` | `'manifest' \| 'pagefind' \| 'orama'` | `'manifest'` | Search backend for `search_content` |
| `search.pagefindBundlePath` | `string` | `'/pagefind/'` | Pagefind bundle path |
| `search.oramaIndexUrl` | `string` | — | URL of pre-built Orama index (required for `'orama'`) |
| `security.exposedTo` | `string[]` | `[]` | Origins allowed to access tools cross-origin |
| `security.maxOutputLength` | `number` | `1500` | Character limit per tool output |
| `security.sanitizeOutputs` | `boolean` | `true` | Strip patterns that resemble prompt injection |

### Custom tools

Each custom tool needs: `name`, `description`, `inputSchema` (JSON Schema), `executeBody` (function body as string), and optional `annotations`. The `executeBody` runs in the browser and receives `params` (tool arguments) and `safeOutput` (sanitization helper). Must return data or a Promise.

### Search backends

`search_content` supports three backends with automatic fallback to manifest:

| Backend | Description | Requires |
|---------|-------------|----------|
| `manifest` (default) | Substring search on the generated manifest | Nothing — always works |
| `pagefind` | Full-text search via Pagefind | `astro-pagefind` or `pagefind` loaded on the page |
| `orama` | Full-text search via Orama | `@orama/orama` + pre-built index at `oramaIndexUrl` |

If the configured backend returns no results or fails, it falls back to manifest search automatically.

## Registered tools

| Tool | Description |
|------|-------------|
| `search_content` | Search articles and pages by keyword (supports manifest, Pagefind, or Orama backends) |
| `list_sections` | List available content sections with item counts |
| `go_to` | Navigate to a specific page by slug (prompts user consent via `requestUserInteraction`) |
| `get_page_info` | Get current page metadata (title, description, headings, language, word count, canonical URL) |
| *your custom tools* | Whatever you define via `customTools` |

### Tool schemas

#### `search_content`

```json
{
  "name": "search_content",
  "inputSchema": {
    "type": "object",
    "properties": {
      "query": { "type": "string", "description": "Search term" },
      "collection": { "type": "string", "description": "Filter by collection (optional)" },
      "limit": { "type": "number", "description": "Max results (default: 5)" }
    },
    "required": ["query"]
  }
}
```

#### `list_sections`

```json
{
  "name": "list_sections",
  "inputSchema": { "type": "object", "properties": {} }
}
```

#### `go_to`

```json
{
  "name": "go_to",
  "inputSchema": {
    "type": "object",
    "properties": {
      "slug": { "type": "string", "description": "Page slug or path" }
    },
    "required": ["slug"]
  }
}
```

#### `get_page_info`

```json
{
  "name": "get_page_info",
  "inputSchema": { "type": "object", "properties": {} }
}
```

## Architecture

```
┌─────────────────────────────────────────────────────────┐
│                      BUILD TIME                          │
│                                                         │
│  Astro pages ────→ Hook astro:build:done                │
│                         │                               │
│                         ▼                               │
│                   /_webmcp/manifest.json                 │
│                   (titles, slugs, descriptions)          │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                    RUNTIME (Browser)                     │
│                                                         │
│  Injected script (head-inline)                          │
│       │                                                 │
│       ├─ fetch('/_webmcp/manifest.json')                │
│       │                                                 │
│       └─ document.modelContext.provideContext({ tools }) │
│            ├── search_content                           │
│            ├── list_sections                            │
│            ├── go_to (+requestUserInteraction)           │
│            └── get_page_info                            │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│                      AI AGENT                           │
│                                                         │
│  Chrome 149+ discovers tools via WebMCP protocol        │
│  Agent can search, list, and navigate site content      │
└─────────────────────────────────────────────────────────┘
```

### Components

**Integration (`src/index.ts`)** — implements `AstroIntegration` with three hooks:

- `astro:config:setup` — injects the client-side script into every page via `injectScript`
- `astro:server:setup` — serves a dynamic manifest during development
- `astro:build:done` — generates `/_webmcp/manifest.json` by extracting titles and descriptions from built HTML

**Client script (`src/client.ts`)** — runs in the browser on every page:

1. Feature detection: `document.modelContext ?? navigator.modelContext`
2. Fetches the manifest from `/_webmcp/manifest.json`
3. Registers 4 tools with JSON Schema input definitions

**Manifest (`/_webmcp/manifest.json`)** — static JSON generated at build time:

```json
{
  "generatedAt": "2026-06-07T00:45:30.260Z",
  "site": "https://example.com",
  "collections": [
    { "name": "blog", "count": 42 },
    { "name": "docs", "count": 15 }
  ],
  "entries": [
    {
      "slug": "blog/my-article",
      "url": "/blog/my-article/",
      "title": "My Article",
      "description": "Article summary",
      "collection": "blog"
    }
  ]
}
```

### Design decisions

| Decision | Rationale |
|----------|-----------|
| Static JSON manifest (not virtual module) | Works for both SSG and SSR, CDN-cacheable, no complex Vite plugin needed |
| Client-side search | No server endpoint needed for small/medium sites ( ({ name: t.name, description: t.description })));

// Chrome 150+
const tools = await document.modelContext.getTools();
console.table(tools.map(t => ({ name: t.name, description: t.description })));
```

### 4. Execute a tool manually

```js
const tools = await navigator.modelContext.getTools();
const search = tools.find(t => t.name === 'search_content');
const result = await navigator.modelContext.executeTool(search, '{"query": "astro"}');
console.log(JSON.parse(result));
```

## Combining with Declarative API

This plugin uses the Imperative API for search and navigation. For existing forms on your site, you can add the Declarative API manually — both coexist:

```html

  Email
  
  Message
  
  Send

```

The agent will see **both** the integration tools + declarative form tools.

## Browser support

| Browser | Status |
|---------|--------|
| Chrome 149+ | ✅ Supported (flag or origin trial) |
| Edge | 🔄 Expected H2 2026 (Microsoft actively collaborating on the spec) |
| Other browsers | ❌ Script exits silently, zero overhead |

The integration is a progressive enhancement. On unsupported browsers, the injected script detects the absence of `navigator.modelContext` / `document.modelContext` and exits immediately. No errors thrown, no extra bytes parsed.

### Enabling WebMCP

WebMCP requires explicit opt-in. There are two paths depending on your use case:

#### Local development: Chrome flag

For testing on your own machine:

1. Open `chrome://flags/#enable-webmcp-testing`
2. Set to **Enabled**
3. Restart Chrome

Tools will appear for any localhost site that registers them. No token needed.

#### Production: Origin Trial

For deployed sites where real visitors should have WebMCP active (without requiring them to flip a flag), Chrome offers an [Origin Trial](https://developer.chrome.com/origintrials/#/register_trial/4163014905550602241).

**Step 1 — Register your origin**

Go to the [WebMCP Origin Trial registration page](https://developer.chrome.com/origintrials/#/register_trial/4163014905550602241) and register the domain(s) where your Astro site is deployed (e.g. `https://mysite.com`). You'll receive a trial token — a long Base64 string.

**Step 2 — Store the token**

Add the token to your `.env` file:

```bash
# .env
WEBMCP_ORIGIN_TRIAL_TOKEN=AjfC0e...your-long-token-here...Qw==
```

Never hardcode the token in source. It's tied to a specific origin and has an expiration date — keeping it in `.env` makes rotation easy.

**Step 3 — Inject the meta tag**

In your base layout (typically `src/layouts/Layout.astro` or `src/layouts/Base.astro`), add the origin trial meta tag inside ``:

```astro
---
// src/layouts/Layout.astro
---

  
    
    

    {/* WebMCP Origin Trial — enables navigator.modelContext for visitors */}
    {import.meta.env.WEBMCP_ORIGIN_TRIAL_TOKEN && (
      
    )}

    My Site
  
  
    
  

```

The conditional (`&&`) means the tag only renders when the token exists — development builds without the env var won't emit an empty meta tag.

**Step 4 — Verify**

After deploying, open DevTools → Application → Frames → top → Origin Trials. You should see "WebMCP" listed as active. Alternatively, check the Console:

```js
// Should return the registered tools if everything works
const tools = await navigator.modelContext.getTools();
console.log(tools.length, 'WebMCP tools active');
```

#### Origin trial caveats

- Tokens are **origin-bound** — `https://mysite.com` and `https://staging.mysite.com` need separate tokens.
- Tokens **expire** (typically 6-12 weeks). Chrome will email you before expiration. Renew and update your `.env`.
- The trial is available starting Chrome 149. Users on older Chrome versions simply won't have `modelContext` — the integration handles this gracefully.
- Native support (no flag or token) is targeted for H2 2026.

## Security

This plugin implements security measures aligned with the [Chrome Agent Security Guidelines](https://developer.chrome.com/docs/agents/security) and [WebMCP Tool Security](https://developer.chrome.com/docs/ai/webmcp/secure-tools) recommendations.

### Security configuration

```js
webmcp({
  security: {
    // Origins allowed to access tools cross-origin (default: none — same-origin only)
    exposedTo: ['https://trusted-partner.com'],

    // Max characters per tool output (default: 1500, per Chrome recommendation)
    maxOutputLength: 1500,

    // Sanitize outputs against indirect prompt injection (default: true)
    sanitizeOutputs: true,
  },
})
```

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `exposedTo` | `string[]` | `[]` | Origins allowed to access tools cross-origin |
| `maxOutputLength` | `number` | `1500` | Character limit per tool output |
| `sanitizeOutputs` | `boolean` | `true` | Strip patterns that resemble prompt injection |

### What is exposed

The plugin only exposes information that is **already publicly accessible** on your site:

| Data | Source | Equivalent to |
|------|--------|---------------|
| Page URLs | Built HTML files | `sitemap.xml` |
| Page titles | `` tag | View source / search engines |
| Descriptions | `` | View source / search engines |
| Headings (h1-h3) | DOM elements | Visible on page |

**The manifest (`/_webmcp/manifest.json`) contains no more information than your `sitemap.xml` already provides to every crawler.**

### What is NOT exposed

- ❌ No server-side data, APIs, or endpoints
- ❌ No authentication tokens or credentials
- ❌ No admin routes or private pages
- ❌ No user data (the plugin is fully static/client-side)
- ❌ No environment variables or build secrets

### Tool annotations

All built-in tools include [security annotations](https://developer.chrome.com/docs/ai/webmcp/secure-tools) to help agents make safe decisions:

| Tool | `readOnlyHint` | `untrustedContentHint` | Rationale |
|------|:-:|:-:|-----------|
| `search_content` | ✅ | ✅ | Read-only; results may contain UGC in titles/descriptions |
| `list_sections` | ✅ | — | Read-only; static collection names controlled by site owner |
| `go_to` | — | — | Mutates state (navigation); agent should confirm with user |
| `get_page_info` | ✅ | ✅ | Read-only; DOM content may include user-generated text |

### Defenses against prompt injection

The plugin implements multiple layers of defense per Chrome's [defense-in-depth](https://security.googleblog.com/2025/12/architecting-security-for-agentic.html) strategy:

#### 1. Deterministic guardrails

- **Output character limits** — All tool outputs are truncated to `maxOutputLength` (default 1.5K chars). This prevents context window overflow and limits the surface area for sophisticated prompt injection attacks.
- **Cross-origin isolation** — Tools are only accessible same-origin by default. Use `exposedTo` to explicitly allowlist trusted origins.
- **Result cap** — `search_content` enforces a maximum of 20 results regardless of the `limit` parameter.

#### 2. Output sanitization

When `sanitizeOutputs: true` (default), the plugin strips common prompt injection patterns from tool outputs:

- `"ignore previous instruc

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [fabricioctelles](https://github.com/fabricioctelles)
- **Source:** [fabricioctelles/astro-webmcp](https://github.com/fabricioctelles/astro-webmcp)
- **License:** MIT
- **Homepage:** https://www.npmjs.com/package/astro-webmcp

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-fabricioctelles-astro-webmcp
- Seller: https://agentstack.voostack.com/s/fabricioctelles
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
