---
title: "MCP and web integrations"
description: "Model Context Protocol (MCP) server integration and web scraping/search configuration for TinyMCE AI on-premises"
canonical_url: "https://www.tiny.cloud/docs/tinymce/latest/tinymceai-on-premises-mcp/"
md_url: "https://www.tiny.cloud/docs/tinymce/latest/tinymceai-on-premises-mcp/index.md"
version: "latest"
last_updated: "2026-08-11T03:33:29Z"
tokens: 7896
---
# MCP and web integrations

The AI service extends model capabilities through two integration points: the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) for tool calling, and pluggable web endpoints for page fetching and search. Both features operate within AI conversations only. Web search and scraping allow the AI to reference live internet content during conversations, and for most deployments, enabling at least web search improves response quality.

These features are configured entirely through environment variables on the AI service container. No additional infrastructure is required beyond a search endpoint.

![MCP and web integrations architecture: TinyMCE editor connects to AI Service through SSE conversations. AI Service connects to LLM provider for inference and to MCP servers and web search and scrape endpoints for tool calling and live web content.](../_images/tinymceai-on-premises/mcp-web-integrations-architecture.svg)

## Demo

The following demo shows the editor experience that [MCP configuration](#mcp-integration) enables. The AI service backing the demo has an MCP server connected to a sample database of acquisition deals and company records. Choosing one of the suggested actions in the Chat sidebar prompts the model to call the MCP server’s tools, then either fill in the document placeholders or answer in Chat.

The editor configuration shown in the **JS** tab applies to on-premises deployments without change. MCP servers are declared on the AI service through [`MCP_SERVERS`](#mcp-integration); the editor requires only the `tinymceai` plugin and a token provider.

**Example**

```js
// The MCP server is configured on the AI service, not in the editor. The editor only
// needs the tinymceai plugin and a token provider.
// The demo-only data plumbing (merge tag list, mentions directory, revision history,
// and template storage) is omitted below to keep the AI configuration in focus.
tinymce.init({
  selector: 'textarea#tinymceai-mcp',
  height: 'clamp(650px, 80vh, 1200px)',
  plugins: [
    'autolink', 'link', 'emoticons', 'lists', 'table', 'advlist', 'searchreplace', 'wordcount', 'autocorrect', 'tinymcespellchecker', 'charmap', 'fullscreen', 'advcode', 'accordion', 'anchor', 'footnotes', 'powerpaste', 'importword', 'exportpdf', 'exportword', 'markdown', 'a11ychecker', 'typography', 'casechange', 'checklist', 'advtable', 'formatpainter', 'permanentpen', 'tableofcontents', 'help', 'math', 'linkchecker', 'media', 'mediaembed',
    'quickbars', 'tinymceai', 'tinycomments', 'uploadcare'
  ],
  toolbar: 'undo redo | tinymceai-chat tinymceai-review | ai-quickactions-translate | styles fontsizeinput | bold italic underline forecolor backcolor casechange | link uploadcare table addcomment | align bullist numlist checklist removeformat | code fullscreen help',
  quickbars_selection_toolbar: 'tinymceai-quickactions ai-quickactions-improve-writing addcomment',
  body_class: 'tox-comments-visible', // Ensure comments show up on init
  sidebar_show: 'tinymceai-chat',
  toolbar_mode: 'sliding',
  visual: false,
  table_default_attributes: {}, // Prevents inserting border=1 attribute on a new table
  content_css: 'content.css',
  user_id: 'james-wilson',
  advcode_inline: true,
  highlight_on_focus: false,
  tinycomments_mode: 'embedded',
  tinymceai_default_model: 'claude-4-6-sonnet',
  tinymceai_allow_model_selection: false,
  tinymceai_chat_welcome_message: 'This demo is connected to an Airtable database containing acquisition deals and related companies information.',
  tinymceai_chat_welcome_actions: [
    { text: 'Try using the following actions with this database:' },
    { text: '**Get help editing your document**' },
    {
      title: 'Fill with "Nuvo Technologies" account details',
      command: 'TinyMCEAIChatPrompt',
      value: {
        prompt: 'Pull the "Nuvo Technologies" record from "TinyMCE demo data" Airtable database and fill in all placeholders in this document',
        displayedPrompt: 'Pull the "Nuvo Technologies" record from Airtable and fill in all placeholders in this document'
      },
      icon: 'document-properties'
    },
    { text: '**Explore data via chat**' },
    {
      title: 'Compile an open deals summary report',
      command: 'TinyMCEAIChatPrompt',
      value: {
        prompt: 'Find all deals in "proposal sent" status and provide a summary in chat with their deal value, notes, confidence level and potential red flags'
      },
      icon: 'comment'
    }
  ],
  tinymceai_languages: [
    { title: 'English', language: 'english' },
    { title: 'Chinese (Simplified)', language: 'chinese' },
    { title: 'Spanish', language: 'spanish' },
    { title: 'German', language: 'german' },
    { title: 'Japanese', language: 'japanese' },
    { title: 'Portuguese', language: 'portuguese' },
    { title: 'Korean', language: 'korean' },
    { title: 'Italian', language: 'italian' }
  ],
  // Required for authentication. Replace with the token endpoint for the AI service.
  tinymceai_token_provider: () => {
    return fetch('/api/tinymceai-token', { credentials: 'include' })
      .then(resp => resp.text())
      .then(token => ({ token }));
  }
});
```

## Prerequisites

This page assumes the following are already configured:

- A running AI service container with database and Redis (see [Database and Redis](../tinymceai-on-premises-database/)).
- At least one LLM provider configured in `PROVIDERS` and `MODELS` (see [LLM providers](../tinymceai-on-premises-providers/)).
- A JWT token endpoint deployed and signing tokens with the correct API Secret (see [JWT authentication](../tinymceai-on-premises-jwt/)).

## Model Context Protocol (MCP)

MCP allows the AI service to call external tools (internal wikis, API specifications, runbooks, contract databases, and compliance checkers) during conversations. The service connects to MCP servers over **Streamable HTTP transport**.

Only Streamable HTTP transport is supported. Stdio-based MCP servers cannot connect directly to the AI service. Wrap them with the SDK’s HTTP transport adapter (for example, `StreamableHTTPServerTransport` in the TypeScript SDK or `StreamableHTTPServer` in the Python SDK) before use.

MCP tools are available in AI **conversations** only. Reviews and quick actions do not invoke MCP tools. For [OAuth-enabled](#mcp-oauth) MCP servers, each user must complete the authorization flow before the server’s tools become available.

### Configuration

Set the `MCP_SERVERS` environment variable to a JSON object. Each key is a server identifier; each value describes the connection. For OAuth-enabled servers, also set `MCP_OAUTH_CALLBACK_URL` (see [OAuth 2.0 authentication](#mcp-oauth)).

```bash
-e MCP_SERVERS='{
  "knowledge-hub": {
    "url": "http://host.docker.internal:3001/mcp",
    "options": { "callToolTimeout": 30 }
  }
}'
```

| Field | Description |
| --- | --- |
| `url` | HTTP endpoint of the MCP server (Streamable HTTP transport). For [OAuth-enabled](#mcp-oauth) servers, this must be the endpoint that supports [OAuth protected resource discovery](https://datatracker.ietf.org/doc/html/rfc9728) (RFC 9728). See [MCP server URL for OAuth](#mcp-oauth-url). |
| `headers` | JSON object of HTTP headers sent with every request (for example, `{"Authorization": "Bearer token"}`). See [Static-token authentication](#mcp-shared-token). |
| `oauth` | OAuth 2.0 configuration block. Enables per-user authorization for MCP servers that require it. See [OAuth 2.0 authentication](#mcp-oauth). |
| `tools.disabled` | Array of tool names to exclude from LLM access. |
| `options.callToolTimeout` | Per-tool-call timeout in seconds (default 60). |

`MCP_SERVERS` supports multiple server entries. Add additional keys to the same JSON object:

```bash
-e MCP_SERVERS='{
  "knowledge-hub": {
    "url": "http://host.docker.internal:3001/mcp"
  },
  "compliance-checker": {
    "url": "http://host.docker.internal:3002/mcp"
  }
}'
```
Each server’s tools are namespaced independently (for example, `knowledge-hub-search_docs` and `compliance-checker-run_check`).

### Docker networking

On Linux Docker, add `extra_hosts: ["host.docker.internal:host-gateway"]` to the AI service compose entry to reach MCP servers running on the host machine.

> **Warning:** `host-gateway` resolves to the Docker bridge IP, not `127.0.0.1`. Host services bound to `127.0.0.1` only (common in Python MCP SDK examples and CLI-style servers) are unreachable from inside the container (`ECONNREFUSED`). Bind MCP servers to `0.0.0.0` or run them as sibling containers on the same Docker network.

### Static-token authentication

The `headers` field sends a fixed set of HTTP headers with every request to the MCP server. Every tool call uses the same credentials regardless of which end user triggered the conversation.

```bash
-e MCP_SERVERS='{
  "knowledge-hub": {
    "url": "http://host.docker.internal:3001/mcp",
    "headers": {
      "Authorization": "Bearer QUkgc2VydmljZQ==",
      "X-Custom-Header": "custom-value"
    }
  }
}'
```
If the MCP server requires per-user context without OAuth, encode identity in the conversation prompt or in a header that the MCP server resolves to a per-user identity on its own side.

### OAuth 2.0 authentication

For MCP servers that require OAuth 2.0 authorization instead of a static access token in the `headers` field, the AI service can act as an OAuth client. The service implements the [OAuth 2.0 Authorization Code flow with PKCE](https://datatracker.ietf.org/doc/html/rfc7636) and supports [Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591) (RFC 7591) for servers that allow it.

End users authorize the connection through their browser. Each user authorizes independently, and the resulting access token is reused on subsequent requests. The AI service refreshes the access token automatically when it expires.

#### MCP server URL for OAuth

When OAuth is enabled, the AI service discovers the authorization server by sending an unauthenticated request to the MCP server `url` and reading the `resource_metadata` link from the `WWW-Authenticate` header in the `401` response (per [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)).

Some MCP servers expose separate endpoints for unauthenticated and OAuth-protected access. The `url` must point to the endpoint that returns this metadata. Using the wrong endpoint causes the `initialize` call to hang indefinitely (see [Troubleshooting](#mcp-troubleshooting)).

> **Tip:** To verify the correct endpoint, send an unauthenticated request and inspect the `WWW-Authenticate` header. It should contain a `resource_metadata` URL: `curl -sI -X POST https://mcp.example.com/v1/mcp/auth -H "Content-Type: application/json" | grep -i www-authenticate`

#### Configuration

Enabling OAuth for an MCP server requires two steps:

1. Set `MCP_OAUTH_CALLBACK_URL` to the URL of the OAuth callback page hosted by the integrating web application. The AI service uses this URL as the OAuth `redirect_uri` parameter for all OAuth-enabled MCP servers (unless overridden per server with `oauth.callbackUrl`).
2. Add an `oauth` block inside the target server entry in `MCP_SERVERS`.

```bash
-e MCP_OAUTH_CALLBACK_URL='https://your-app.example.com/oauth/callback' \
-e MCP_SERVERS='{
  "knowledge-hub": {
    "url": "http://host.docker.internal:3001/mcp",
    "headers": { "Authorization": "Bearer static-token" }
  },
  "external-tools": {
    "url": "https://mcp.example.com/v1/mcp",
    "oauth": {
      "clientId": "<oauth-client-id>",
      "clientSecret": "<oauth-client-secret>",
      "scopes": ["read", "write"]
    }
  }
}'
```
Internal MCP servers can use static-token authentication (via `headers`) alongside external servers that require OAuth. Each server is configured independently within the same `MCP_SERVERS` object. `MCP_OAUTH_CALLBACK_URL` only applies to servers with an `oauth` block.

| Field | Description |
| --- | --- |
| `oauth.clientId` | OAuth 2.0 client identifier issued by the MCP server. Omit to perform [Dynamic Client Registration](#mcp-oauth-dcr). |
| `oauth.clientSecret` | OAuth 2.0 client secret. Used together with `clientId` for confidential clients. |
| `oauth.scopes` | Array of OAuth scopes to request from the authorization server. Omit to use default server scopes. |
| `oauth.callbackUrl` | Per-server override of `MCP_OAUTH_CALLBACK_URL`. Use when different MCP servers redirect to different pages in the web application. |

#### Dynamic Client Registration

If the MCP server supports [RFC 7591 Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591), `clientId` and `clientSecret` can be omitted from the `oauth` block. The AI service registers itself with the MCP server automatically on the first authorization attempt and reuses the resulting registration for subsequent flows.

#### Authorization flow

The OAuth flow is driven by the integrating web application. The AI service exposes REST endpoints (see [REST endpoints](#mcp-oauth-endpoints)); the application is responsible for orchestrating the redirects and forwarding the authorization code.

1. **Initialize.** The application calls `POST /v1/mcp/oauth/{serverName}/initialize` for the signed-in user. The response contains either:

  - `{ "connected": true }` — the user already has a valid access token; no further action is required.
  - `{ "connected": false, "authorizationUrl": "…​" }` — redirect the user’s browser to `authorizationUrl`.
2. **User consents.** The MCP server’s authorization page opens in the user’s browser. After approval, the server redirects back to `MCP_OAUTH_CALLBACK_URL` (or `oauth.callbackUrl`) with `code` and `state` query parameters appended.
3. **Complete.** The callback page extracts `code` and `state` from the query string and forwards them to `POST /v1/mcp/oauth/{serverName}/complete` with body `{ "code": "…​", "state": "…​" }`. On success, the user’s connection becomes active and subsequent AI conversations can use the MCP server’s tools.

> **Note:** The AI service does not host the callback page. Register `MCP_OAUTH_CALLBACK_URL` (or each per-server `oauth.callbackUrl`) as an authorized redirect URI with the MCP server — either through the server’s developer console or, with Dynamic Client Registration, automatically.

> **Important:** The authorization session is valid for ten minutes after `initialize`. If the user takes longer to finish the authorization page, the `complete` call returns an `mcp-oauth-invalid-state` error and the flow must be restarted from `initialize`.

#### Callback page implementation

The AI service does not host the callback page. The integrating web application must serve a page at the `MCP_OAUTH_CALLBACK_URL` that extracts the `code` and `state` query parameters from the redirect URL and forwards them to the AI service’s `complete` endpoint.

Minimal callback page
```html
<script>
  const params = new URLSearchParams(window.location.search);
  fetch('/api/mcp/oauth/my-server/complete', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ code: params.get('code'), state: params.get('state') })
  })
    .then(r => r.json())
    .then(data => document.body.textContent = data.connected ? 'Connected.' : 'Failed: ' + JSON.stringify(data));
</script>
```
Replace `my-server` with the key from `MCP_SERVERS`. In production, add error handling, a loading indicator, and validation for missing `code` or `state` parameters.

In the example above, the callback page calls `/api/mcp/oauth/{serverName}/complete` on the integrating application’s own server rather than directly on the AI service. This is the proxy pattern described below.

#### Browser integration and proxy endpoints

The [OAuth REST endpoints](#mcp-oauth-endpoints) on the AI service require JWT authentication and may not be directly callable from the browser due to CORS restrictions. In browser-based integrations, proxy the OAuth calls through the application’s own backend.

The backend generates a JWT (the same token used for conversations), attaches it to the request, and forwards the call to the AI service. This keeps the JWT generation server-side and avoids CORS configuration on the AI service.

A typical integration requires four proxy routes:

- `GET /api/mcp/oauth/status` → `GET {AI_SERVICE_URL}/v1/mcp/oauth/status`
- `POST /api/mcp/oauth/{serverName}/initialize` → `POST {AI_SERVICE_URL}/v1/mcp/oauth/{serverName}/initialize`
- `POST /api/mcp/oauth/{serverName}/complete` → `POST {AI_SERVICE_URL}/v1/mcp/oauth/{serverName}/complete`
- `DELETE /api/mcp/oauth/{serverName}` → `DELETE {AI_SERVICE_URL}/v1/mcp/oauth/{serverName}`

#### REST endpoints

All endpoints are authenticated with the same JWT used for other AI service endpoints. The acting user is identified by the token; each user has an independent connection state per MCP server.

| Method | Path | Description |
| --- | --- | --- |
| `GET` | `/v1/mcp/oauth/status` | Returns the connection status for every OAuth-enabled MCP server, with `connected` and an optional `expiresAt` timestamp for the calling user. |
| `POST` | `/v1/mcp/oauth/{serverName}/initialize` | Starts the OAuth flow. Returns `{ "connected": true }` or `{ "connected": false, "authorizationUrl": "…​" }`. |
| `POST` | `/v1/mcp/oauth/{serverName}/complete` | Finishes the OAuth flow. Body: `{ "code": "…​", "state": "…​" }`. On success, returns `{ "connected": true }`. |
| `DELETE` | `/v1/mcp/oauth/{serverName}` | Removes the stored token and any in-progress authorization state for the calling user. |

These endpoints are available once OAuth is configured for at least one MCP server.

#### Connection scope and revocation

OAuth connections are established per user and do not carry over between users or environments.

To revoke a user’s connection, send `DELETE /v1/mcp/oauth/{serverName}`.

For OAuth-enabled MCP servers, the connection is established only after each user completes the authorization flow. Until then, the server’s tools are not exposed to that user.

### Tool namespacing

MCP tools are exposed to the LLM with a namespaced identifier: `<server_id>-<tool_name>` (for example, `knowledge-hub-search_knowledge_base`). The `tools.disabled` array accepts the **bare tool name** only (for example, `"search_knowledge_base"`, not the namespaced form).

### MCP server example

The following example uses the official [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) (`@modelcontextprotocol/sdk`) with `StreamableHTTPServerTransport`, which handles session management, SSE framing, and the full [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#streamable-http) specification.

<!-- <details> -->
<!-- <summary> -->
Knowledge-base MCP server
<!-- </summary> -->
Create `package.json`:

```json
{
  "name": "knowledge-mcp-server",
  "type": "module",
  "private": true,
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.29.0",
    "express": "^4.18.0",
    "zod": "^3.23.0"
  }
}
```
Install dependencies:

```bash
npm install
```
Create `knowledge-mcp-server.js`:

```javascript
// knowledge-mcp-server.js
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
import { z } from 'zod';

const KNOWLEDGE_BASE = {
  'api-guidelines': 'All REST APIs must use JSON, include pagination through Link headers, and return 4xx for client errors with a machine-readable error code.',
  'deployment-process': 'Deployments require: 1) PR approval, 2) passing CI, 3) staging verification, 4) production canary (5% traffic for 30min), 5) full rollout.',
  'security-policy': 'All user data must be encrypted at rest (AES-256) and in transit (TLS 1.3). PII requires additional field-level encryption.',
};

function createServer() {
  const server = new McpServer({
    name: 'knowledge-hub',
    version: '1.0.0'
  });

  server.tool(
    'search_knowledge_base',
    'Search the company knowledge base for policies, guidelines, and procedures',
    { query: z.string().describe('Search query') },
    async ({ query }) => {
      const q = query.toLowerCase();
      const results = Object.entries(KNOWLEDGE_BASE)
        .filter(([key]) => key.includes(q) || q.includes(key.split('-')[0]))
        .map(([key, value]) => `## ${key}\n${value}`)
        .join('\n\n');
      return {
        content: [{ type: 'text', text: results || 'No results found.' }]
      };
    }
  );

  server.tool(
    'get_api_spec',
    'Get the OpenAPI spec for an internal service',
    { service: z.string().describe('Service name (for example user-service, billing-api)') },
    async ({ service }) => {
      return {
        content: [{ type: 'text', text: `Spec not found for: ${service}` }]
      };
    }
  );

  return server;
}

const app = express();
app.use(express.json());

app.all('/mcp', async (req, res) => {
  const server = createServer();
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
  await server.close();
});

app.listen(3001, () => console.log('Knowledge MCP server on http://0.0.0.0:3001/mcp'));
```

<!-- </details> -->
The example server binds to `0.0.0.0` so it is reachable from inside Docker containers. When the MCP server runs on the host machine, the AI service connects to it at `http://host.docker.internal:3001/mcp` (see [Docker networking](#mcp-docker-networking)).

## Web search

The AI service can forward search queries to an external endpoint, enabling AI conversations to reference live web content.

### Enabling web search

Web search requires three components configured together:

| Component | Configuration |
| --- | --- |
| Environment variables | Set `WEBSEARCH_ENABLED='true'` and `WEBSEARCH_ENDPOINT` on the AI service container. Optionally set `WEBSEARCH_REQUEST_TIMEOUT` (default 10000ms) and `WEBSEARCH_HEADERS` for authenticated search backends. |
| MODELS entry | The model’s `MODELS` entry must include `capabilities.webSearch: true` (boolean) for the web search toggle to appear in the editor. See [MODELS configuration](../tinymceai-on-premises-providers/#models-required). |
| JWT permission | The JWT must include `ai:conversations:webSearch` in the `auth.ai.permissions` array. See [JWT permissions](../tinymceai-on-premises-jwt/#permissions-reference). Without this permission, the toggle remains grayed out even when the environment variables and MODELS entry are correct. |

```bash
-e WEBSEARCH_ENABLED='true' \
-e WEBSEARCH_ENDPOINT='http://host.docker.internal:4001/search' \
-e WEBSEARCH_REQUEST_TIMEOUT='10000' \
-e WEBSEARCH_HEADERS='Authorization: Bearer search-api-key, X-Source: tinymce'
```

> **Important:** `WEBSEARCH_HEADERS` uses **colon-CSV format** (`Header-Name: value, Another: value`), not JSON. Passing a JSON object produces `ERR_INVALID_HTTP_TOKEN` and silently prevents all outbound search requests.

### Per-message activation (custom integrations)

The TinyMCE editor sends the web search activation flag when the user toggles web search on. Custom integrations that call the AI service API directly must include the following in each message request body where web search should be active:

```json
{
  "capabilities": { "webSearch": {} }
}
```
The value **must** be an empty object (`{}`). This is the documented API contract (see [CKEditor AI Models: Capability Configuration](https://ckeditor.com/docs/cs/latest/guides/ckeditor-ai/models.html#capability-configuration)). The service rejects other shapes:

- `"webSearch": true`: rejected (schema expects an object, not a boolean).
- `"webSearch": {"enabled": true}`: rejected (unrecognized key).
- Omitted entirely: web search is not surfaced to the model, even when all server-side configuration is correct.

Without this field, the environment variables, JWT permission, and MODELS configuration are insufficient. The model never receives the web search tool.

### Endpoint contract

| Direction | Payload |
| --- | --- |
| Request | JSON object with a `query` field (search string). |
| Response | JSON object with a `results` array; each item includes `url`, `text`, `title`, and optional `author`, `publishedAt`, and `favicon`. |

Request body
```json
{ "query": "search string" }
```
Response body
```json
{
  "results": [
    {
      "url": "https://example.com/article",
      "text": "Content snippet",
      "title": "Article Title",
      "author": "Author",
      "publishedAt": "2026-04-30T10:00:00Z",
      "favicon": "https://example.com/favicon.ico"
    }
  ]
}
```

### Implementation example (SerpAPI)

```javascript
// search-service.js
const express = require('express');
const app = express();
app.use(express.json());

app.post('/search', async (req, res) => {
  const response = await fetch(
    `https://serpapi.com/search.json?q=${encodeURIComponent(req.body.query)}&api_key=${process.env.SERP_API_KEY}`
  );
  const data = await response.json();
  const results = (data.organic_results || []).slice(0, 5).map(r => ({
    url: r.link,
    title: r.title,
    text: r.snippet
  }));
  res.json({ results });
});

app.listen(4001);
```

## Web scraping

The AI service can forward web page fetches to an external endpoint, enabling AI conversations to reference specific pages.

### Enabling web scraping

Set `WEBRESOURCES_ENABLED='true'` and `WEBRESOURCES_ENDPOINT` on the AI service container. Optionally set `WEBRESOURCES_REQUEST_TIMEOUT` (default 10000ms).

```bash
-e WEBRESOURCES_ENABLED='true' \
-e WEBRESOURCES_ENDPOINT='http://host.docker.internal:4000/scrape' \
-e WEBRESOURCES_REQUEST_TIMEOUT='10000'
```

### Trigger mechanism

The AI service calls the scrape endpoint (`WEBRESOURCES_ENDPOINT`) when it receives a request on:

```none
POST /v1/conversations/{id}/web-resources
Content-Type: application/json

{ "url": "https://example.com/page-to-fetch" }
```
The TinyMCE editor sends this request when a user pastes or references a URL in conversation. Custom integrations must call this endpoint explicitly to trigger a page fetch.

The response is stored against the conversation. The `type` field in the scrape response must be `text/html` or `text/markdown`. Other MIME types (for example, `application/pdf`) are rejected with a `422 web-resource-download-error`.

### Endpoint contract

| Direction | Payload |
| --- | --- |
| Request | JSON object with a `url` field (page to fetch). |
| Response | JSON object with `type` (`text/html` or `text/markdown`) and `data` (body content). |

Request body
```json
{ "url": "https://example.com/article" }
```
Response body
```json
{ "type": "text/html", "data": "<html><body><p>Example page body</p></body></html>" }
```

### Implementation example (Playwright)

```javascript
// scraper-service.js
const { chromium } = require('playwright');
const express = require('express');
const app = express();
app.use(express.json());

app.post('/scrape', async (req, res) => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto(req.body.url, { waitUntil: 'networkidle' });
  const content = await page.content();
  await browser.close();
  res.json({ type: 'text/html', data: content });
});

app.listen(4000);
```

## SSE events

Custom streaming UI integrations can use the following Server-Sent Events (SSE) to render tool call progress and search results. The AI service emits these events during conversations:

| Event name | Description |
| --- | --- |
| `mcp-tool-result` | Emitted when an MCP tool call completes. Contains the tool result. There is no pre-call event; clients receive no signal until the tool call finishes. |
| `web-search` | Emitted when a web search returns results (or fails without emitting an error event; see [Troubleshooting](#web-search-troubleshooting)). |
| `source` | Emitted with source citations from the model response. |

## Kubernetes deployment

In Kubernetes, the `extra_hosts: host-gateway` Docker pattern does not apply. Deploy MCP servers and web integration endpoints as sibling Deployments with ClusterIP Services in the same namespace as the AI service. Reference them using cluster DNS:

```bash
-e MCP_SERVERS='{
  "knowledge-hub": {
    "url": "http://mcp-knowledge.tinymce-ai.svc.cluster.local:3001/mcp",
    "options": { "callToolTimeout": 30 }
  }
}'
-e WEBRESOURCES_ENDPOINT='http://web-scrape.tinymce-ai.svc.cluster.local:4000/scrape'
-e WEBSEARCH_ENDPOINT='http://web-search.tinymce-ai.svc.cluster.local:4001/search'
```
Store `MCP_SERVERS` and `WEBSEARCH_HEADERS` in a Kubernetes Secret and mount them as environment variables in the AI service Deployment. This avoids exposing credentials in the Deployment manifest and allows rotation without redeploying.

For production clusters, the MCP server Deployments should have dedicated resource requests, liveness probes, and replica counts independent of the AI service. See [Production deployment](../tinymceai-on-premises-production/) for general Kubernetes patterns.

## Troubleshooting

| Symptom | Resolution |
| --- | --- |
| Web search or web scraping features do not appear at all | `WEBSEARCH_ENABLED` and `WEBRESOURCES_ENABLED` both default to `false`. Set them to `true` in the AI service environment and provide the corresponding `_ENDPOINT` values. Without these flags enabled, the AI service disables all web integration features regardless of JWT permissions or MODELS configuration. |
| Web search toggle is grayed out in the editor | The JWT is missing the `ai:conversations:webSearch` permission. Add it to the `auth.ai.permissions` array in the token (or use the wildcard `ai:conversations:*`). See [JWT permissions](../tinymceai-on-premises-jwt/#permissions-reference). |
| Web search is toggled on but the model never uses it | The per-message request body is missing `"capabilities": {"webSearch": {}}`. The TinyMCE editor sends this when the toggle is active. Custom integrations must include it in each request body. See [Per-message activation (custom integrations)](#websearch-per-message). |
| `ERR_INVALID_HTTP_TOKEN` in AI service logs related to web search | `WEBSEARCH_HEADERS` is set as a JSON object. The service expects colon-CSV format: `Header-Name: value, Another: value`. |
| `ECONNREFUSED` when connecting to MCP server | The MCP server is bound to `127.0.0.1`. Bind to `0.0.0.0` instead, or run the MCP server as a sibling container on the same Docker network (or Kubernetes namespace). |
| `[WARN] MCP client health check ping failed, evicting` every ~60 seconds | Non-functional. The AI service re-handshakes with the MCP server after each eviction. Tool calls continue to work. This log volume is expected in steady state and does not indicate a problem. |
| Web search returns "no results" but the search back end is running | Check AI service container logs for `[WARN] Web search request failed`. When the `WEBSEARCH_ENDPOINT` returns a 5xx error, the service logs a warning but emits an empty `event: web-search` to the client with no error payload. The model proceeds as if zero results were returned. |
| `422 web-resource-download-error` from the web-resources endpoint | This single error code covers multiple failure modes: the scrape endpoint returned 5xx, returned non-JSON, returned an empty `data` field, or returned an unsupported `type` (anything other than `text/html` or `text/markdown`). Check AI service container logs for the underlying cause. |
| `mcp-oauth-invalid-state` error from the `complete` endpoint | The authorization session expired. Sessions are valid for ten minutes after calling `initialize`. Restart the flow from `POST /v1/mcp/oauth/{serverName}/initialize`. |
| MCP tools not appearing for a user after OAuth is configured | The user has not completed the authorization flow. OAuth connections are per user — each user must authorize independently through `initialize` and `complete`. Check the connection status with `GET /v1/mcp/oauth/status`. |
| `initializeMcpOAuth` hangs for 60+ seconds with no response | The MCP server `url` in `MCP_SERVERS` does not return OAuth resource discovery metadata ([RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728)). The AI service sends an unauthenticated request to the URL and expects a `401` response with a `WWW-Authenticate` header containing a `resource_metadata` link. If the header is missing, the service hangs indefinitely. Verify the correct endpoint with: `curl -sI -X POST https://mcp.example.com/endpoint -H "Content-Type: application/json"` and check for `resource_metadata` in the `WWW-Authenticate` header. Some MCP servers expose separate URLs for authenticated and OAuth-protected access. See [MCP server URL for OAuth](#mcp-oauth-url). |

## See also

- [LLM providers](../tinymceai-on-premises-providers/): provider configuration and the `MODELS` catalog
- [Reference](../tinymceai-on-premises-reference/): full environment variable reference including `MCP_SERVERS`, `WEBRESOURCES_*`, and `WEBSEARCH_*`
- [Troubleshooting](../tinymceai-on-premises-troubleshooting/): general troubleshooting
