Skip to main content
Glama
madelponte

openwebui-tools MCP server

by madelponte

openwebui-tools MCP server

A single MCP server that bundles five tool groups (originally written for Open WebUI), so they can be used from any MCP-capable client (Claude Desktop, IDEs, custom agents, Open WebUI's MCP support, etc.). fetch_page doubles as a YouTube transcript fetcher, and the email tool is send-only.

Built on FastMCP v4. The default transport is streamable-http, so the server is reachable over the network at http://<host>:8000/mcp. Everything is configured from a single YAML file (config.example.yamlconfig.yaml), which is bind-mounted into the container.

Tools

Tool group

MCP tools exposed

Agentic Web Search

search_web, fetch_page

Stock Data

get_company_data

Wolfram Alpha

query_wolfram_alpha

Place Search

find_nearby_places

Email

send_email

Each tool can be independently omitted from MCP registration with a flag in the config file's tools: section. For example, set search_web_enabled: false and fetch_page_enabled: false to replace those two with third-party tools. The available flags are search_web_enabled, fetch_page_enabled, get_company_data_enabled, query_wolfram_alpha_enabled, find_nearby_places_enabled, and send_email_enabled (as environment variables: SEARCH_WEB_ENABLED, FETCH_PAGE_ENABLED, …). All default to true; restart the server after changing them.

Every tool is context-budget aware: list/range parameters are maximums, not fixed amounts. The model can request less per call, and anything above the server-configured cap is silently clamped so an oversized response can't overwhelm a model's context window. Omitting a value uses the cap.

search_web(query, time_range=None, country=None, search_lang=None, safesearch=None, context_threshold_mode=None, num_results=None, max_tokens=None) — Search with Brave's LLM Context API, which returns relevance-ranked excerpts extracted from source pages for direct model consumption. Excerpts may contain text, tables, code, or JSON-serialized structured data. Each result carries a URL, title, snippets list, optional published date, description, and site name. The top-level provider is brave_llm_context.

time_range accepts day/week/month/year/all or an inclusive custom YYYY-MM-DD to YYYY-MM-DD range. country, search_lang, safesearch (off/moderate/strict), and context_threshold_mode (strict/balanced/lenient/disabled) map directly to Brave options. num_results controls the source-URL count and max_tokens controls the approximate total excerpt budget; both are clamped to configured server caps. Search uses only Brave's excerpts and metadata; it does not fetch source pages. Call fetch_page(url, mode="structured") when you need a page outline, then use section= to read a specific section.

Migration note: enrich_results and the settings web_search.max_enrich_results, web_search.default_enrich_results, and web_search.enrich_max_bytes have been removed. An old config file that still mentions them only logs a warning and starts anyway. web_search.max_enrich_headings remains as the historical name for fetch_page's heading cap; it does not enable search enrichment.

Brave LLM Context does not expose result-page pagination or search categories. Put those constraints in the query instead—for example site:youtube.com for videos, which fetch_page can then read as transcripts. Brave supports operators such as site:, filetype:, intitle:, inbody:, lang:, loc:, quoted phrases, exclusion with -, and uppercase AND/OR/NOT; operators are experimental and overly restrictive combinations may return no results.

fetch_page(url, mode="text", section=None, query=None, max_matches=None, context_lines=None, include_match_toc=false, offset=None) — Fetch the contents of a single page (or a URL returned by search_web). Reads one URL per call — to read several pages, call the tool once per URL. mode="text" returns the page as markdown — headings, lists, tables, and hyperlinks (resolved to absolute URLs) are preserved, so the model sees the page's structure and can fetch a link it found in the content (set web_search.markdown: false for bare plain text). Prominent images are replaced at their original positions by explicit [Image at this location: ...] markers populated from page-provided alt text, captions, or image metadata; these are textual stand-ins, not visual analysis. Standalone image URLs return the same placeholder and any embedded SVG description when available. Extracted headings carry visible {#anchor} markers so a downstream agent can cite a precise section. Source-native heading IDs are usable as URL fragments; generated cite-* anchors are stable identifiers only within the returned extraction. Structured headings include citation_url when the source page supplied a real fragment. mode="structured" returns metadata only (title, description, heading outline, JSON-LD, and prominent image descriptions). Document links (PDF, Word, Excel, PowerPoint, OpenDocument, RTF, EPUB) are extracted via Apache Tika and always returned as text. Passing a section (a heading from a page_headings outline) returns just that section of an HTML page instead of the whole thing. Passing a query (a keyword, phrase, or regex) returns only bounded extractive match windows. max_matches controls how many windows are shown and context_lines controls surrounding nonblank lines; both are clamped to server-configured safe limits. Each window reports 1-based line ranges, exact match lines, its surrounding heading, and a compact quality label (exact_line, literal_substring, or regex_pattern). Set include_match_toc=true to return a small TOC containing only matching headings, transcript timestamps, or document line ranges. YouTube matched segments retain their [M:SS] timestamps. Regex evaluation has a hard total time budget; a pattern that exceeds it raises a tool error instead of returning an incomplete scan. If a response is marked truncated, pass offset with the returned next_offset to read the next chunk; this works for HTML, documents, JSON, Reddit, and transcripts. A YouTube video URL returns the video's transcript rather than the watch page (see below).

Reddit URLs are returned as compacted JSON. When Reddit OAuth is configured, fetch_page uses the authenticated Data API first. It otherwise falls back in order to Reddit's RSS feed, targeted old.reddit.com HTML extraction, and official oEmbed metadata. RSS and old Reddit may expose only an initial comment snapshot; the result reports comment counts/completeness when the source makes them available.

Fetching is resilient: a direct httpx request first, an automatic FlareSolverr fallback for bot/CAPTCHA walls and JavaScript-empty pages, then a last-resort Firecrawl API fallback when local fetching cannot recover usable content. This ordering keeps Firecrawl usage low enough for a small API quota. A short page cache lets an agent loop that re-fetches the same URL skip the network round-trip.

Fetching is also SSRF-guarded. Because a URL can come from search results or page content the model just read, it's attacker-influenceable via indirect prompt injection — so fetch_page resolves the target host and refuses any non-public address (loopback, private, link-local, etc.), blocking access to localhost, cloud metadata endpoints like 169.254.169.254, and LAN hosts. The check is applied to the initial URL and every redirect hop (a public URL can't 302 into an internal one). Direct fetches also verify the connected peer IP so a DNS rebinding race cannot swap a public lookup for a private connect. FlareSolverr and Firecrawl follow redirects inside their own browsers — that request cannot be intercepted — but the provider-reported final URL is SSRF-checked and the body is discarded if it landed on a blocked host. To deliberately allow a trusted local/private target you host, list its host, IP, or CIDR in web_search.ssrf_allowlist (e.g. [localhost, 127.0.0.1, 10.0.0.0/8]).

Concurrent fetch_page work is also bounded: web_search.max_concurrent_direct_fetches, max_concurrent_flaresolverr, max_concurrent_tika, and max_concurrent_firecrawl cap in-flight sidecar/API calls so a model that fans out many reads cannot stampede FlareSolverr or Tika.

Stock Data

get_company_data(symbol, sections=None, statement="income", period="annual", periods=None, news_items=None, insider_weeks=None, history_bars=None, news_days=None, history_interval="1d", financial_metrics=None) — One company, or a short list of companies for comparison, with only the sections you ask for. symbol accepts a ticker (AAPL), company name (Apple), or a list of tickers/names; names are resolved to tickers via symbol search before any data is fetched, so there's no separate lookup step. Available sections:

  • quote — latest price, day's change, open/high/low/previous close, volume.

  • profile — name, sector, industry, market cap, employees, exchange, and key fundamentals (P/E, EPS, dividend yield, 52-week range, beta, margins).

  • financials — income statement, balance sheet, or cash flow, controlled by statement (income/balance/cashflow) and period (annual/quarterly); periods sets how many to return. Set financial_metrics to a list or comma-separated string (for example, ["revenue", "gross profit", "free cash flow"]) to return only matching rows and keep the response compact. The response includes a metrics_filter block listing matched and unmatched requested metrics.

  • earnings — historical earnings: actual vs. estimated EPS, surprise %, revenue. periods sets how many to return.

  • news — recent articles (headline, source, summary, url, published date). news_items sets how many to return; news_days sets the lookback window.

  • insiders — insider buying/selling with a buy/sell summary and individual transactions. insider_weeks sets how far back to look.

  • price_history — recent OHLC price bars (newest first); history_bars sets how many bars to return, and history_interval controls bar size (1d, 1wk, or 1mo).

  • peers — competitor/peer tickers in the same sector or industry (Finnhub).

  • dividends — dividend payment history plus stock splits (yfinance).

  • ownership — ownership summary and top institutional holders (yfinance).

Defaults to ["quote", "profile"] when sections is omitted. Data is sourced across providers (Finnhub / yfinance / FMP) with optional yfinance fallback. On partial success the response includes an errors map listing sections that returned nothing; if every requested section fails, the call raises an error so a failure is never mistaken for data. When symbol was a company name, the response includes a resolved_from block naming the matched company (and any alternatives) so you can confirm the right ticker was used. For a list input, the response is {"results":[...]} and one failed ticker does not sink the whole comparison unless every ticker fails.

Note: earlier versions exposed get_stock_quote, get_company_profile, get_financials, get_earnings, get_company_news, and search_symbol as separate tools. These are now folded into the single get_company_data tool — the data tools via the sections parameter, and search_symbol via automatic name→ticker resolution on the symbol argument. This keeps the tool count low (better for smaller models' tool selection) and lets one call do what used to take two or more.

Wolfram Alpha

query_wolfram_alpha(query, assumption=None, units=None) — Exact computation and authoritative reference data: math, unit/currency conversion, physics & chemistry, astronomy, geography & demographics, dates & times, finance, nutrition, weather history, linguistics, and structured entity comparisons. Queries should be English keyword-style ("France population", not a full sentence). If a result returns assumptions, re-send the same input with the relevant assumption value to disambiguate. units may be metric or nonmetric; omit it to use the server default.

YouTube transcripts (via fetch_page)

There is no separate YouTube tool — pass a YouTube video URL to fetch_page and it returns the video's transcript / closed captions instead of scraping the watch page, for summarizing, quoting, searching, or translating. Any YouTube URL form works (watch, youtu.be, /shorts/, /embed/, /live/). Preferred languages come from youtube.default_languages (falling back to any available transcript). Transcripts are cached (they almost never change), and optional Webshare / generic proxy settings are supported for networks where YouTube blocks the server's IP. Folding this into fetch_page keeps the tool count low, which helps smaller models avoid tool-selection paralysis.

find_nearby_places(category="", near=None, latitude=None, longitude=None, radius_m=None, limit=None, nearby_towns_limit=None, place_details=False) — Find points of interest near a location via OpenStreetMap Overpass. Specify the location either as near (a place name, geocoded for you via Nominatim — so "vegan restaurants in Portland" is a single call), as near="lat,lon", as a map URL containing coordinates, as an OpenStreetMap node/way/relation URL, or as explicit latitude/longitude (which win if both are given). category is plain language, not OSM tags: restaurant, coffee, pharmacy, atm, hotel, museum, gas station, etc. A food category can be prefixed with a diet — vegan, vegetarian, halal, kosher, or gluten free ("vegan restaurant", or just "vegan"). An unrecognized category falls back to matching place names, so brands like "Starbucks" work too. Results are sorted nearest-first and include distance plus useful tags (cuisine, address, phone, website, opening hours) when available. An empty results list means nothing matched in range (not an error). Every POI search automatically includes nearby city/town/village centers that can seed follow-up searches in neighboring municipalities; nearby_towns_limit can reduce the number returned.

Set place_details=true to look up rich information about the place named in near or at the supplied coordinates instead of searching for POIs around it. That mode returns coordinates, bounding box, address details, population when available, Wikidata/Wikipedia links, website, phone, and alternatives; it ignores category, radius_m, and limit.

The server has no access to the user's location, so a relative near value ("near me", "nearby", "around here", etc.) is refused with a message telling the model to ask the user where to search or pass explicit coordinates — rather than silently guessing a location.

It uses the public OpenStreetMap APIs by default and honors Nominatim's usage policy: a descriptive geocoding.user_agent (set this!) and a ~1 req/sec throttle on the public API. To self-host, point geocoding.nominatim_url / geocoding.overpass_url at your own instances, clear geocoding.overpass_fallback_urls if queries must stay private, and set geocoding.min_request_interval_seconds: 0. Results are cached (place data changes slowly), which also eases the rate limits.

Related MCP server: mcp-web-calc

Configuration

The whole server is configured by one YAML file. Copy the example and edit it:

cp config.example.yaml config.yaml

config.example.yaml lists every setting with its default and a comment. Keep config.yaml out of git (it is .gitignored) — it holds API keys, the SMTP password, and the bearer tokens that gate the HTTP transports.

Where the file is read from, first match wins:

  1. $MCP_CONFIG_FILE — an explicitly named file must exist; a typo fails at startup instead of silently booting on defaults. This is what the Dockerfile sets (/app/config.yaml) and what docker-compose.yml bind-mounts.

  2. config.yaml / config.yml next to server.py (the repo root).

  3. /etc/mcp-server/config.yaml — for a host-managed file mounted without touching the compose file.

Finding no file at all is fine: every setting has a default, and HTTP transports still refuse to start without a bearer token.

Layout, precedence, and environment overrides

Settings are grouped into one section per tool group:

YAML section

Covers

Env prefix

tools:

which tools get registered

(none)

server:

transport, logging, bearer auth

MCP_

web_search:

search_web and fetch_page

WEB_SEARCH_

stock:

get_company_data

STOCK_

wolfram:

query_wolfram_alpha

WOLFRAM_

youtube:

transcript retrieval inside fetch_page

YOUTUBE_

geocoding:

find_nearby_places

GEO_

email:

send_email

EMAIL_

Precedence for any single value is process environment variable → YAML file → built-in default. The variable name is the section's prefix plus the key in upper case, so the pre-YAML names keep working and a secret can stay out of the mounted file: web_search.brave_api_keyWEB_SEARCH_BRAVE_API_KEY, geocoding.max_radius_mGEO_MAX_RADIUS_M, tools.send_email_enabledSEND_EMAIL_ENABLED. A variable set to an empty value counts as unset, so a stale FOO= inherited from a shell or image layer cannot wipe a YAML value. Nested/list settings such as server.auth_tokens are YAML-only.

Migrating from .env: the server no longer loads .env, and Compose no longer passes it to the MCP container. Copy your values into the corresponding YAML sections, or explicitly inject overrides with Compose environment: / env_file: or Docker --env-file. Exported process variables still work even without a YAML file when running locally. The image requires its configured MCP_CONFIG_FILE mount unless that variable is explicitly cleared.

About the format:

  • Invalid values abort startup with a message naming the section, the key, and — when it came from the environment — the responsible variable. Caps are range-constrained in config.py, so a misconfiguration fails at boot instead of mid-request.

  • Unknown keys are logged as warnings and ignored, so a file kept from an older release still starts a newer server. Duplicate keys (including alternate case/hyphen spellings) are rejected instead of silently overwriting settings.

  • Settings that took comma-separated lists also accept YAML lists: geocoding.overpass_fallback_urls, web_search.ssrf_allowlist, email.allowed_recipients, youtube.default_languages. Entries must be strings; quote language codes such as "no" that YAML interprets as booleans.

  • Quote words YAML reads as booleans: brave_safesearch: "off", not brave_safesearch: off. Bare off/on/yes/no/true/false are boolean values in YAML, and the startup error says to quote them.

Key things to set:

  • server.auth_tokens — at least one named bearer token, or the HTTP transports refuse to start. See Authentication.

  • wolfram.app_id — required for the Wolfram tool (free AppID).

  • stock.finnhub_api_key — recommended for Stock Data (improves name→ticker resolution and quote/profile coverage; everything falls back to keyless yfinance).

  • stock.fmp_api_key — optional Financial Modeling Prep key; when set, financial statements (financials section) are sourced from FMP instead of yfinance.

  • web_search.brave_api_key — required for search_web; create a Search API subscription token at Brave Search API. brave_api_url, localization/filter defaults, search candidate count, token budgets, and timeout are separately configurable. Calls are serialized with a default one-second quiet period (brave_request_delay_seconds) for low-throughput plans; HTTP 429/502/503/504 and transient transport failures receive bounded exponential retries configured by brave_max_retries / brave_retry_backoff_seconds, honoring Brave's reset headers.

  • web_search.firecrawl_api_key — optional fetch_page credential; enables the last-resort fetch fallback when the first-line FlareSolverr HTML render is blocked/unusable or a known document is hidden behind an HTML challenge. Firecrawl is not used by search_web. firecrawl_hedge_enabled / firecrawl_hedge_delay_seconds optionally start Firecrawl while a slow FlareSolverr render is still running; disabled by default to avoid unnecessary credits.

  • web_search.classifier_api_url / classifier_model — optional OpenAI-compatible small-model classifier for ambiguous rendered pages; both must be set to enable it. classifier_api_key supplies an optional bearer token.

  • web_search.circuit_breaker_* — configure the short-lived host circuit that skips FlareSolverr after repeated failures when Firecrawl is available.

  • web_search.reddit_client_id / reddit_client_secret / reddit_user_agent — optional Reddit OAuth credentials; strongly recommended for reliable Reddit post/comment fetching. See Reddit Data API setup.

  • web_search.reddit_request_delay_seconds — serializes Reddit acquisitions and leaves a quiet period between calls (default 1) to reduce anonymous RSS/HTML burst throttling; 0 disables queueing. Post RSS/oEmbed URLs are also canonicalized so share/context parameters cannot bypass the raw-page cache.

  • web_search.reddit_rate_limit_retry_seconds — after an anonymous RSS HTTP 429, wait this long (default 3) and retry once before degrading to old Reddit/oEmbed; 0 disables the retry. These throttling mitigations are not a substitute for OAuth on hosted-server IPs.

  • web_search.ssrf_allowlist — optional; hosts/IPs/CIDRs that fetch_page may reach despite the SSRF guard's default block on non-public addresses (e.g. a local page you host). Empty by default (all private/loopback/link-local targets blocked).

  • geocoding.user_agent — set a descriptive User-Agent (ideally with contact info) as required by Nominatim's usage policy. Also set nominatim_email to a contact address (recommended by the policy so they can reach you before blocking on heavy use). Self-hosters should also set nominatim_url / overpass_url, clear overpass_fallback_urls when queries must stay private, and set min_request_interval_seconds: 0.

  • email.username / email.password — required for send_email. For Gmail, password must be a 16-character App Password, not the normal account password. from_address, from_name, SMTP host/port/TLS, timeout, and recipient/attachment caps are configurable. Set email.allowed_recipients (addresses and/or domains) on any network-exposed server. Attachments stay off until email.attachment_root points at a directory the tool may read.

  • web_search.max_concurrent_* — in-flight caps for direct fetches, FlareSolverr, Tika, and Firecrawl (defaults 8 / 2 / 2 / 2).

Reddit Data API setup

Reddit blocks unidentified API traffic from many hosted-server networks. A personal, non-commercial project can request free Data API access, subject to Reddit's approval and rate limits:

  1. Sign in to the Reddit account that will own the application.

  2. Read Reddit's Developer Platform and Data API access guidance and Responsible Builder Policy.

  3. Submit Reddit's Data API access request. Describe this as a personal, non-commercial MCP page reader and provide the repository URL if requested. Follow any approval instructions Reddit sends you.

  4. Open Reddit app preferences, select create another app, and create a confidential application. For a personal server, the script type is normally appropriate. The redirect URI is not used by this server's application-only flow, but Reddit may still require a valid URL such as http://localhost:8080.

  5. Copy the short value displayed beneath the application name as the client ID, and copy the value labeled secret as the client secret.

  6. Add the credentials to the web_search: section of config.yaml, replacing your_username with the owning Reddit username:

    web_search:
      reddit_client_id: "your_client_id"
      reddit_client_secret: "your_client_secret"
      reddit_user_agent: "linux:mcp-server:1.0 (by /u/your_username)"
  7. Restart the MCP server. Do not commit config.yaml or expose the client secret.

The server exchanges these credentials for a short-lived application-only OAuth token, caches it until shortly before expiration, and sends requests to oauth.reddit.com. It does not store or require your Reddit password. Reddit's current free-access limit is 100 queries per minute per OAuth client ID; consult the Data API Wiki for current requirements.

Email

send_email(recipients, subject, body, cc=None, bcc=None, reply_to=None, attachments=None) sends a plain-text email through the configured SMTP account. It is send-only: it cannot read, list, or delete mailbox contents. recipients, cc, and bcc are lists of email addresses; BCC recipients are included in the SMTP envelope but not written into message headers. Set email.allowed_recipients to a list of addresses and/or domains so a prompt-injected model cannot mail arbitrary people; when that list is set, To/Cc/Bcc/Reply-To outside it are rejected. Attachments are disabled unless email.attachment_root is set; paths must stay inside that directory (symlink escapes are rejected), and the result reports only the filename, not the resolved filesystem path. Counts are still capped by email.max_attachments and email.max_attachment_bytes.

The result reports status (sent or partial), intended recipients by field, attempted recipients, accepted recipients, refused recipients with SMTP codes and server responses, invalid addresses, dropped addresses, and attachment metadata. SMTP authentication, sender, connection, or total-recipient-refusal failures raise tool errors instead of being returned as successful sends.

Debug mode

Set server.debug: true to enable debug mode: tool responses are serialized as indented, human-readable JSON (instead of compact JSON) and each tool call emits verbose per-call logs to stdout. Reddit fetch_page results also append a redacted fallback trace to note, showing whether OAuth JSON, RSS, old Reddit, and oEmbed were skipped, failed, or succeeded. Useful for troubleshooting; leave it off in normal operation so responses stay compact in the model's context window.

Tool-catalog caching

FastMCP 4 advertises that opted-in modern clients may reuse the static MCP component catalog for server.tool_catalog_cache_ttl_seconds (default 300 seconds), reducing repeated tools/list round trips. Set it to 0 to disable the hint. server.tool_catalog_cache_scope defaults to public because every authenticated caller currently sees the same tools; use private if visibility ever varies by caller. These settings do not cache tool-call results—the provider-specific TTL caches remain separate.

Tool-name prefix in cross-references

Some MCP clients prepend a namespace to every tool name before showing it to the model — Open WebUI, for example, forces an mcp_ prefix, so fetch_page appears to the model as mcp_fetch_page. The server keeps its tool names bare (prefixing them here too would double it, e.g. mcp_mcp_fetch_page), but a few docstrings point one tool at another (e.g. search_web tells the model to use fetch_page to read a result). server.tool_prefix is the prefix spliced into those cross-references so they match what the model actually sees. It defaults to blank (no prefix); set it to mcp_ when serving Open WebUI, or to whatever prefix your client adds. The value is inserted verbatim, so include any trailing separator (e.g. the _).

Authentication

HTTP transports require at least one bearer token. Clients must send an Authorization: Bearer <token> header; anything else gets a 401. Starting streamable-http or SSE with no token configured is a startup error. Set server.allow_unauthenticated: true only for a tightly firewalled local setup (the server logs a warning in that case). Tokens are ignored for the stdio transport, which has no network surface.

Tokens are named, one entry per client (or per trust boundary):

server:
  auth_tokens:
    - name: open-webui
      token: "9f1c…"   # openssl rand -hex 32
    - name: claude-desktop
      token: "a77b…"

Any listed token authenticates, and the matched name is logged with the request at DEBUG level (enable server.debug or set server.log_level: DEBUG), so you can tell clients apart in docker logs. Revoke one credential by deleting its entry and restarting — without resetting anybody else's. Names are not secrets, must be unique, and a blank token is a startup error rather than an empty credential. Each token is compared against every configured entry with a constant-time compare.

Generate a strong token per client:

openssl rand -hex 32

The single-token server.auth_token field (and the MCP_AUTH_TOKEN environment variable) still work and appear in the log as a client named default; prefer auth_tokens once you have more than one client.

Open WebUI per-user valves and UI-only behaviors that don't apply to MCP were dropped: status/progress events, citation events, the Wolfram HTML result "card" (it now returns plain text), and the stock tool's verbose_status / include_raw_numbers per-user toggles.

The compose file starts the server plus the local services used by fetch_page: FlareSolverr (Cloudflare fallback) and Apache Tika (document text extraction). Tika is pinned to apache/tika:4.0.0-1-full; the other supplied services use published images with latest tags. Pin those tags/digests too if you need reproducible deployments. search_web uses Brave's hosted API and requires web_search.brave_api_key. HTTP mode also requires at least one server.auth_tokens entry. The container reads its configuration from ./config.yaml, bind-mounted read-only at /app/config.yaml:

cp config.example.yaml config.yaml   # then edit it
docker compose up -d

The MCP endpoint is then available at http://localhost:8000/mcp.

The server process runs as uid 10001, so config.yaml must be world-readable or otherwise readable by that uid. On a Linux host, either:

sudo setfacl -m u:10001:r config.yaml   # keep 0600 for the owner
chmod 644 config.yaml                   # simpler, but any local user can read it

A missing or unreadable config.yaml is a startup failure, not a silent fall back to defaults; Compose will not create a directory in place of the file. After editing configuration, run docker compose up -d --force-recreate mcp-server to reload it. Recreating also refreshes a single-file bind mount when an editor replaces the file atomically. Compose uses the published server image; to test local source changes, build and run your own image as shown below.

If you don't need fetch_page, delete the flaresolverr / tika services (and the depends_on block) from docker-compose.yml. The stock, Wolfram, geocoding, and email tools have no local-service dependencies, though they may need API keys, SMTP credentials, or internet access.

Tika 4 document extraction

The supplied tika-config.json runs one parser worker with a 2 GiB heap inside a 4 GiB container limit. It keeps the standard parsers and Tesseract OCR; no VLM, Ollama, or other model backend is configured. Tika must remain on a trusted private network: server.allowPerRequestConfig is enabled so the MCP client can select an OCR strategy per request. /pipes and /async remain off.

fetch_page posts multipart file + JSON config to /tika/config/json/md and reads Tika 4's tk:content. The JSON envelope also exposes parser failures and native PDF character counts. Returned documents keep format: "document_text" for compatibility and add content_format: "markdown"; Markdown headings/lists/tables are preserved. Document query/offset still work, but document outline/section extraction is not implemented.

Default behavior (web_search settings):

  1. tika_ocr_strategy: no_ocr: extract native text, disabling Tesseract even for embedded images on the first pass.

  2. tika_ocr_retry: true: when there is no usable text, retry once with OCR_AND_TEXT_EXTRACTION and Tesseract enabled. A scanned PDF's metadata title does not count as native page text or successful OCR.

  3. If OCR still finds no text, raise a tool error. HTTP/parser failures, truncated extraction, and output-cap violations are errors, not OCR retries.

Set tika_ocr_retry: false (or WEB_SEARCH_TIKA_OCR_RETRY=false) to keep no_ocr strictly text-only. For mixed PDFs with both text and scanned pages, choose tika_ocr_strategy: auto instead: the empty-document retry intentionally does not run when some native page text exists. Explicit OCR strategies run once. Standalone image URLs retain their existing metadata-only tool behavior.

max_concurrent_tika defaults to 1, matching the worker count. A busy Tika worker (HTTP 429) gets at most two retries honoring delay-seconds Retry-After within the pass budget; worker failures (503) are not automatically retried. Each pass has its own tika_timeout_seconds budget (default 90 seconds), and local capacity wait is also bounded by that setting. The bundled server config caps parsing at 75 seconds per pass. Large scans may need both limits raised, along with the calling MCP client's tool deadline. The streamed JSON response, including metadata, obeys max_download_bytes.

Migration: deploy the updated MCP image, Tika 4 image, and JSON config together. This client no longer supports Tika 3 or its removed OCR headers. If sharing Tika with Open WebUI, use a build supporting TIKA_SERVER_VERSION=4 and select version 4 in its effective configuration. Ordinary requests without MCP's overrides use server-side AUTO OCR, preserving scanned-upload support for Open WebUI. Its built-in Tika loader still requests plain text; the Markdown behavior described here is for MCP.

Run with Docker (server only)

docker build -t openwebui-tools-mcp .
docker run --rm -p 8000:8000 \
  -v "$PWD/config.yaml:/app/config.yaml:ro" \
  openwebui-tools-mcp

The image expects its configuration at /app/config.yaml (MCP_CONFIG_FILE), so the mount is required; override that variable to read the file from another path. Never add config.yaml to the image — it holds credentials (it is already in .dockerignore).

Run locally (no Docker)

python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp config.example.yaml config.yaml   # then edit it
python server.py

config.yaml next to server.py is found automatically, whichever directory you start the process from. Set server.transport: stdio (or MCP_TRANSPORT=stdio) to run as a stdio MCP server instead — useful for clients that spawn the process directly rather than connecting over HTTP — or sse for Server-Sent Events transport. stdio has no network surface, so no token is required.

Connecting a client

For an HTTP client, point it at http://<host>:8000/mcp (streamable-http). For example, a Claude Desktop / generic client config using a stdio bridge or native streamable-http support would reference that URL. Give each client its own server.auth_tokens entry and configure it to send an Authorization: Bearer <token> header (most MCP clients expose a "headers" or "auth token" field for HTTP servers) — that is what makes the two of them distinguishable in the log. For stdio mode, configure the client to launch python server.py; it reads the same config.yaml (point MCP_CONFIG_FILE at it if it lives elsewhere).

License

MIT

Related MCP Connectors

Related MCP Servers

  • F
    license
    C
    quality
    C
    maintenance
    A multi-tool MCP server that enhances local LLMs with web search, document reading, scholarly research, Wikipedia access, and calculator functions. Provides comprehensive tools for information retrieval and computation without requiring API keys by default.
    22
    1
    -
  • A
    license
    A
    quality
    D
    maintenance
    An MCP server that enables web searching, URL content extraction, and summarization without requiring API keys. It also provides advanced mathematical evaluation and multi-language Wikipedia summary retrieval tools.
    5
    362 npm
    6
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    A comprehensive MCP server providing 15 web tools including search, scraping, screenshots, SEO audits, and DNS/SSL checks through a single installation. It delivers clean, LLM-optimized outputs so AI agents can focus on reasoning rather than parsing raw HTML.
    15
    6 npm
    MIT