Skip to main content
Glama

MCP Nexus

Browse-first MCP middleware — an LLM-friendly nexus for discovering and invoking MCP tools across multiple services.

AI agents use this as a single MCP endpoint to browse, inspect, and call tools from many upstream MCP servers — without flooding their context with every tool schema upfront.

How It Works

Instead of connecting every MCP server directly (and loading all their tool schemas at session start), agents connect to one nexus server and discover tools on demand:

browse_services              → [{id: "todoist", name: "Todoist"}, {id: "outlook", ...}]
browse_tools("todoist")      → ["todoist__get-task", "todoist__create-task", ...]
search_tools("send email")   → [{name: "outlook__search-emails", serviceId: "outlook"}, ...]
get_schemas(["todoist__get-task"])  → [full input schema]
call_tool("todoist__get-task", {id: "123"}) → result

Agents can either browse (list services → list tools) or search (find tools by keyword or semantic similarity across all services at once).

Related MCP server: mcptool

Quick Start

Prerequisites

  • Node.js 22+

Install & Run

# Install dependencies
npm install

# Copy a config (or create your own)
cp mcp-nexus.example.yaml mcp-nexus.yaml

# Start in dev mode (with hot reload)
npm run dev

# Or with a custom config path and verbose logging
npx tsx src/index.ts --config ./mcp-nexus.example.yaml --verbose

Verify It's Running

# Health check
curl http://localhost:8050/health

Configuration

Create a mcp-nexus.yaml file:

port: 8050

auth:
  enabled: false # Set to true and provide a token in production
  token: ""
  allowedOrigins: # Optional — restrict CORS to these origins when auth is on
    - https://openwebui.local

connectors:
  httpReuseIdleTimeoutSeconds: 300 # Reap idle upstream HTTP sessions after N seconds
  recoveryIntervalSeconds: 30 # Probe failed sources every N seconds (0 = disabled)

search:
  type: lexical # "lexical" (keyword matching) or "semantic" (embedding-based)
  maxResults: 20
  # semantic: # Uncomment to enable semantic search
  #   provider: built-in # "built-in" (local), "ollama", or "openai-compatible"
  #   model: Xenova/all-MiniLM-L6-v2
  #   batchSize: 32
  #   # For ollama: provider: ollama, baseUrl: http://ollama:11434, model: nomic-embed-text
  #   # For openai-compatible: provider: openai-compatible, baseUrl: https://api.openai.com, model: text-embedding-3-small, apiKeyEnv: OPENAI_API_KEY

sources:
  - id: todoist
    name: Todoist
    description: Task and project management
    transport: http
    url: http://todoist-mcp:8081/mcp
    filter: ["*"] # Glob patterns — only index matching tools

  - id: outlook
    name: Outlook
    description: Email and calendar
    transport: stdio
    command: npx
    args: ["-y", "@softeria/ms-365-mcp-server"]
    env:
      API_KEY: "your-key"
    preloadedTools:
      - search-emails
      - list-folders

Config Reference

Field

Description

port

HTTP port for the MCP endpoint (default: 8050)

auth.enabled

Require Authorization: Bearer <token> header

auth.token

Static bearer token (override via MCP_NEXUS_AUTH_TOKEN env var)

auth.allowedOrigins

Optional list of origins allowed via CORS when auth is enabled. If omitted, the request Origin is reflected back

connectors.httpReuseIdleTimeoutSeconds

Idle timeout before a cached upstream HTTP session is reaped (default: 300)

connectors.recoveryIntervalSeconds

Interval (seconds) for background recovery probes of failed sources. 0 = disabled (default: 30)

search.type

Search strategy: "lexical" (keyword matching, default) or "semantic" (embedding-based similarity)

search.maxResults

Max results returned by search_tools (default: 20)

search.semantic.provider

Embedding provider: "built-in" (local model), "ollama", or "openai-compatible" (required if type is semantic)

search.semantic.model

Model name (provider-specific; defaults vary by provider)

search.semantic.baseUrl

Base URL for ollama or openai-compatible providers (required for those providers)

search.semantic.apiKeyEnv

Name of env var containing the API key (required for openai-compatible)

search.semantic.batchSize

Batch size for embedding generation at index time (default: 32)

search.semantic.modelCachePath

Where to cache the downloaded model (built-in provider only)

sources[].id

Unique identifier for the source (used in namespaced tool names)

sources[].transport

"http" for Streamable HTTP, "stdio" for subprocess

sources[].url

Upstream MCP server URL (required for HTTP transport)

sources[].command

Executable to spawn (required for stdio transport)

sources[].filter

Optional glob patterns to curate which tools are indexed

sources[].preloadedTools

Optional array of non-prefixed tool names to surface directly in tools/list (e.g. [\"search-emails\"])

sources[].projections

Optional default response projections, keyed by non-prefixed tool name (see Response Shaping)

artefacts.root

Directory artefacts are written under. Omit the whole artefacts block to disable the feature (see Artefacts)

artefacts.retentionDays

Delete run directories older than this, at startup and daily (default: 14; 0 = never)

artefacts.runIdleMinutes

How long a label keeps resolving to the same run directory (default: 180)

artefacts.maxBytes

Refuse to write a single artefact larger than this (default: 33554432)

The search_tools tool lets agents find tools by query instead of browsing every service. Two strategies are available, configured at startup via search.type:

Lexical (default)

Keyword matching against tool names and descriptions. Fast, no dependencies. Best for queries like "send email" or "ebay orders" — concise terms that appear in the tool metadata.

search:
  type: lexical
  maxResults: 20

Semantic

Embedding-based similarity search. Understands natural-language intent like "I want to send an email" or "find tools for managing my inbox". Requires an embedding provider.

search:
  type: semantic
  maxResults: 20
  semantic:
    provider: built-in # local model, no external dependencies
    model: Xenova/all-MiniLM-L6-v2
    batchSize: 32
    modelCachePath: /app/data/model-cache

Embedding Providers

Provider

Description

Config

built-in

Local model via Transformers.js (all-MiniLM-L6-v2, 384d)

No external dependencies. Downloads model on first run.

ollama

Local Ollama instance (nomic-embed-text, 768d)

Requires baseUrl (e.g. http://localhost:11434)

openai-compatible

Any OpenAI-compatible API (text-embedding-3-small, 1536d)

Requires baseUrl, apiKeyEnv, and model

If the semantic provider fails at query time (e.g. Ollama is down), the search engine falls back to lexical automatically. The response includes strategy and fellBackToLexical fields so the agent can tell what happened.

Docker

# Build
npm run docker:build

# Run
docker run -d \
  --name mcp-nexus \
  -p 8050:8050 \
  -v ./mcp-nexus.yaml:/app/mcp-nexus.yaml \
  -e MCP_NEXUS_AUTH_TOKEN=your-token \
  mcp-nexus

Or use the provided Dockerfile directly:

docker build -t mcp-nexus .

MCP Tools

The nexus exposes these tools to connected AI agents:

Tool

What it does

browse_services

List all available upstream services with descriptions and tool counts

browse_tools

List all tools for a specific service (namespaced names)

search_tools

Search for tools by keyword (lexical) or natural language (semantic)

get_schemas

Get input schemas and inferred response shapes for one or more tools

call_tool

Call a tool on an upstream service, optionally trimming the response or writing it to a file

index

Diagnostic — shows index summary, source availability, and error info

Additionally, any tools listed under preloadedTools on a source will appear directly in the tools/list response alongside the built-in nexus tools — no browsing needed.

Response Shaping

Upstream tools routinely return far more than an agent needs — every field of every record, often pretty-printed. That width lands directly in the agent's context, so the nexus trims it on the way through.

Always applied. Responses are forwarded as the upstream service's own content blocks, with each JSON block re-serialised compactly. This is lossless — nothing is dropped, and blocks that aren't JSON (prose errors, images, embedded resources) pass through untouched. On live eBay responses this alone removes 38–48% of the bytes.

Projections. To trim fields as well, give call_tool a select array of dotted paths. [*] maps over an array, and the original nesting is preserved:

{
  "toolName": "ebay__ebay_get_inventory_items",
  "parameters": { "limit": 25 },
  "select": ["total", "inventoryItems[*].sku", "inventoryItems[*].product.title"]
}

A path matching nothing is returned as an error, not as absent data, so a typo can't be mistaken for a field the service doesn't return. The error carries the tool's response shape so the caller can correct itself.

For tools that are always too wide, set a default under sources[].projections instead — keyed by the tool's own name, applied to every call, and overridden by an explicit select. A configured projection whose paths have drifted out of date warns and skips them rather than failing, since the caller didn't write it.

Discovering paths. Most services declare no outputSchema, so get_schemas reports a responseShape instead: the leaf paths and types of what the tool last returned, learned from calls as they pass through. Its size is fixed regardless of how many records came back. If a tool hasn't been called yet, make one small call first (most take a limit or pageSize) and the shape will be recorded.

Artefacts

Projections trim a response; artefacts remove it from the conversation altogether. When a result is only ever going to be aggregated by code — a full orders feed, a whole catalogue — passing an artefacts label writes it to a file and returns a receipt instead:

{
  "toolName": "ebay__ebay_get_orders",
  "parameters": { "limit": 25, "offset": 50 },
  "artefacts": "ebay-weekly-review"
}
{
  "artefact": {
    "run": "20260824-161204-ebay-weekly-review",
    "dir": "/data/artefacts/20260824-161204-ebay-weekly-review",
    "path": "/data/artefacts/20260824-161204-ebay-weekly-review/ebay_get_orders-a3f1c92b.json",
    "bytes": 18206,
    "records": 25,
    "recordPath": "orders",
    "shape": ["total: number", "orders[*].lineItems[*].legacyItemId: string", "…"]
  }
}

The agent then runs code against dir. Nothing about the payload enters its context.

This requires a code executor that can see the same absolute path — mount one volume into both containers at the same location. Read-only on the executor's side is the cleanest arrangement: artefacts are the nexus's output and its input.

artefacts is a label, not a path. The caller names the task; the nexus builds the directory name from a timestamp and the label reduced to [a-z0-9-], and resolves it strictly under root. Every call sharing a label lands in one directory until it has been idle for runIdleMinutes, so a multi-call pull needs no coordination — and last week's run can never be read as this week's.

Filenames are derived from the tool and a digest of its arguments, so a retried page overwrites itself instead of leaving a duplicate for the aggregation to double-count.

records is the count of the largest top-level array, reported per file so a caller can check that pages sum to the expected total without opening anything. A page past the end of a feed reports 0 rather than going missing.

Projections still apply. The context argument for trimming disappears, but the reason to keep buyer addresses out of a response is not that they are expensive. select still overrides a configured projection, and shape describes what is actually in the file — not the wider upstream response, which get_schemas still reports in full.

Errors are never written to a file. Transport failures, upstream tool errors and unmatched select paths all come back inline, as they do without a label. If the write fails, the call returns an error naming the path and errno — the payload is deliberately not returned instead, since dumping a whole feed into the context is the failure the caller was avoiding.

Preloaded tools take no artefacts argument (or select), since they are dispatched with the upstream schema verbatim. A tool wide enough to want either should be reached through call_tool.

Architecture

AI Agent ──Streamable HTTP──▶ mcp-nexus ──HTTP/stdio──▶ todoist, outlook, ...
                                  │
                              In-memory index
                              Session management
  • Transport: MCP Streamable HTTP (2025-11-05)

  • Auth: Optional bearer token, with optional CORS origin allowlist

  • Health: GET /health endpoint for monitoring (Uptime Kuma, etc.)

  • HTTP connection reuse: keep-alive sessions per source, reaped after an idle timeout

Project Structure

src/
  index.ts              Entry point with CLI args
  config.ts             YAML loader with Zod validation
  types.ts              Shared types and interfaces
  logger.ts             Structured logger
  namespace.ts          Tool name namespacing (<sourceId>__<toolName>)
  glob-utils.ts         Glob pattern matching for tool filtering
  indexer.ts            Startup index — fetches tools/list from all sources
  artefacts.ts          Run directories, artefact writing, retention
  recovery.ts           Background recovery probes for failed sources
  nexus-server.ts       MCP server — tool definitions and request handling
  sources/
    http-source.ts      HTTP transport client (Streamable HTTP)
    stdio-source.ts     Stdio transport client (subprocess, JSON-RPC)
  search/
    index.ts            SearchEngine — strategy dispatch + fallback
    types.ts            Search config, result, and provider interfaces
    lexical-search.ts   Keyword matching (token-based scoring)
    semantic-search.ts  Embedding similarity search
    providers/
      builtin.ts        Transformers.js (all-MiniLM-L6-v2, local)
      ollama.ts         Ollama embedding API (nomic-embed-text)
      openai.ts         OpenAI-compatible embedding API

Scripts

Command

Description

npm run dev

Run with hot reload via tsx watch

npm start

Run without watch

npm run build

Compile TypeScript to dist/

npm run docker:build

Build Docker image

npm run docker:run

Run Docker container

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    An MCP aggregator that consolidates multiple MCP servers behind a single interface with just 3 tools (search, get details, execute), reducing context pollution for AI agents by avoiding direct exposure of numerous tool schemas.
    21
    2
    MIT
  • A
    license
    Not graded
    quality
    Not graded
    maintenance
    A drop-in MCP proxy that aggregates multiple backend servers into two meta-tools for efficient tool discovery and execution. It enables AI clients to access hundreds of tools while minimizing context window usage through searchable indexing.
    1
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enterprise-grade dynamic MCP proxy that eliminates token bloat by lazy-loading tool schemas based on semantic intent, enabling efficient orchestration of multiple backend tools from a single endpoint.
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    A proxy MCP server that manages multiple upstream MCP servers by grouping them and loading tool schemas on-demand, reducing LLM context usage.
    2,798
    51
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Aidan-Kay/mcp-nexus'

If you have feedback or need assistance with the MCP directory API, please join our Discord server