Nexus
Provides access to the live GitHub REST API, handling per-user credentials, GitHub's two distinct rate limits, and Link-header pagination.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@NexusShow me orders from Acme Corp placed in the last 30 days."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Nexus — a remote MCP server for heterogeneous enterprise systems
A remote MCP server (streamable HTTP, OAuth 2.1) exposing three deliberately different backends through one tool surface, built to answer one question from experience rather than from reading:
How is an MCP server different from a REST API wrapper?
The short answer is three things, in increasing order of how few people can say them: discovery happens at runtime rather than build time; the consumer is a model, which inverts the design constraints; and the protocol has primitives — elicitation, and the multi-round-trip pattern behind it — that a wrapper cannot express at all.
Status: milestone 2 of 4. All three systems are live, all three tool surfaces are built from one operations table, and the measurement harness runs. The accuracy numbers are not yet measured — that needs an
ANTHROPIC_API_KEY, and this README will say so rather than quote a guess. The token-overhead half is measured, below. Elicitation and deployment are milestones 3 and 4.
The claim this project is organised around
The tool surface is part of the prompt.
A REST API is designed around resources and CRUD, for a developer who reads documentation, can retry, and can ask a colleague. A model reads tool descriptions at inference time and gets one shot. That changes everything downstream:
For a developer | For a model |
Endpoint-shaped tools are fine — compose them | Every extra tool is context spent and a chance to mis-select |
Docs are reference material | Descriptions are prompt engineering |
| An error must teach a retry strategy |
Response size is bandwidth | Response size is context budget |
Pagination is | Page numbers invite the model to invent |
Every design decision below follows from the right-hand column.
Related MCP server: Relay
The three systems — heterogeneity is the design principle
Three backends chosen because they fail in three different ways. This is what makes the project a design study rather than a wrapping exercise.
System | What it is | The failure mode it forces |
Structured | Postgres — orders, customers, products, shipments, returns. 420 seeded orders over 14 months. | Unbounded scans, exfiltration queries, the accidental cross join |
External SaaS | The real GitHub REST API, per-user credentials, live | Two different rate limits, Link-header pagination, flakiness you didn't simulate |
Unstructured | 30 documents / 360 chunks of the MCP specification, embedded with | Retrieval, not lookup — the answer is a ranked passage, not a record |
The Postgres data is synthetic, and that is stated rather than hidden: there is no public transactional orders database to point at. The other two are real.
GitHub earns its place by being actually external. The client distinguishes GitHub's two rate limits, which is the bug most wrappers ship:
the primary limit is a quota —
x-ratelimit-remainingcounts down, and you can plan around it from headers you already have;the secondary limit is abuse detection — it fires on burst rate, returns 403 not 429, and appears in no counter beforehand.
A retry loop that treats the second like the first hammers the endpoint until it is blocked outright.
Architecture
MCP client (Claude Code / Cursor / your own)
│ streamable HTTP + Bearer token
▼
┌─────────────────────────────────────────────┐
│ Express │
│ requireBearerAuth ──► verifyAccessToken │ OAuth 2.1 resource server
│ mcpAuthMetadataRouter (RFC 9728) │ (never an auth server)
├─────────────────────────────────────────────┤
│ createMcpHandler(buildServer) │
│ └─ ONE McpServer per request, │
│ registering only the tools this │ ◄── per-caller tool visibility
│ caller's scopes permit │
├─────────────────────────────────────────────┤
│ tools/ find_orders get_order_detail │ task-shaped
│ query_metrics │ the deliberate escape hatch
├─────────────────────────────────────────────┤
│ contracts/ errors (retryable + hint + │
│ alternatives), paging (caps, │
│ total_matched, signed cursors) │
├─────────────────────────────────────────────┤
│ data/ SET LOCAL ROLE ─► nexus_reader │
│ allowlisted api.* views only │
│ EXPLAIN cost gate ─► refuse │
└─────────────────────────────────────────────┘
│
▼ Postgres 16 + pgvector
ops.* base tables — no tool-facing role has any privilege here
api.* the published viewsThe three questions this is built to answer well
1. "Why not just give the model raw SQL?"
The answer is not "never". It is a stated trade-off with controls, in order of how much each actually buys:
A least-privileged role is the only real boundary. Every query runs inside a
BEGIN READ ONLYwithSET LOCAL ROLE nexus_reader, a role with no privilege on theopsschema at all.SET LOCAL, notSET, so a pooled connection can never be handed to the next request still wearing the previous caller's role.Published views, not base tables.
api.*is what a model may see, decided column by column —v_customersdeliberately has noemail.A mandatory outer LIMIT, wrapped rather than parsed.
A statement timeout, transaction-local.
EXPLAINcost gating. The query is planned, the planner's own cost estimate is read, and anything above the threshold is refused before it executes. In practice this catches the accidental cross join far more often than anything adversarial — which is exactly why it earns its place.
Everything except (1) is a usability layer: it turns a permission error the
model cannot act on into an instruction it can. Verified, not asserted —
tests/sqlGuard.test.ts asks Postgres directly, bypassing every check in our
own code:
✓ nexus_reader cannot read a base table even by direct query (42501)
✓ nexus_reader cannot execute the write function (42501)
✓ nexus_writer still cannot read a base table directly (42501)
✓ the transaction is read-only, so even a privileged statement cannot mutate
✓ refuses an unbounded cross join before executing it (cost 7.1e9 > 5e4)2. "How does auth work?"
The model is not the principal, the user is.
Nexus is an OAuth 2.1 resource server and never an authorization server. An unauthenticated request gets the full challenge chain:
$ curl -i -X POST localhost:8080/mcp -d '...'
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token",
resource_metadata="http://localhost:8080/.well-known/oauth-protected-resource/mcp"Token verification validates the audience (RFC 8707) — a token minted for
another API is not a token for this one, however valid its signature — and always
populates expiresAt, because requireBearerAuth treats an unset expiry as an
invalid token.
Tool visibility is filtered per caller, which the 2026-07-28 spec explicitly
permits: the tool set "MAY vary by the authorization presented on the request …
since credentials are per-request input, not connection state." This is not a
filter applied to a fixed list — createMcpHandler's per-request factory
receives the verified authInfo and registers a different tool set, so a
tool the caller cannot use never enters the model's context at all:
# orders:read only # orders:read + metrics:read
- find_orders - find_orders
- get_order_detail - get_order_detail
- query_metricsIt is a real boundary, not a display convenience — calling a hidden tool by name
returns Tool query_metrics not found, because it was never registered.
The list is served cacheScope: "private" (a shared cache holding one user's
tool list and serving it to another would leak a capability listing across a
security boundary) and in deterministic order, which the spec asks for so
clients can cache and LLM prompt caches hit.
3. "What happens when the model gets it wrong?"
Two layers: prevention through fewer, better-differentiated tools, and
recovery through errors that carry a strategy. Every error answers three
questions — is retrying useful, what should change, what else could work — and
is returned as a tool execution error (isError: true), never a JSON-RPC
protocol error, because the spec says clients pass the former to the model for
self-correction and that the latter is "less likely to result in successful
recovery".
The counterpart failure is a successful empty result, which a model reads as "no such records exist" when the truth is nearly always "your filter was wrong":
$ find_orders("Northwind orders in 2019")
{ "results": [], "total_matched": 0,
"hint": "matching customers exist (Northwind Traders), so another filter
excluded them; 40 order(s) match everything except the date range
— the window is probably wrong" }The measurement
The same capability set, built at three granularities from one operations table,
crossed with two description qualities. npm run surfaces:
surface | tools | capabilities | tuned defs | weak defs |
| 81 | 33 | 38,266 chars | 37,394 chars |
| 16 | 33 | 15,845 chars | 10,237 chars |
| 7 | 33 | 9,255 chars | 3,865 chars |
Three things about this table are load-bearing:
All three reach exactly the same 33 capabilities. tests/surfaces.test.ts
fails if they diverge. Comparing selection accuracy across surfaces that can do
different things would measure scope, not granularity.
The endpoint surface has 81 tools for 33 capabilities, and that redundancy is
the point rather than padding. Real endpoint-shaped servers accumulate
get_order, lookup_order_by_id, list_orders, search_orders_by_customer,
list_cancelled_orders — each added the day someone needed it, each reading
plausibly to a model facing a question it half-matches. A human disambiguates
near-duplicates by reading docs; a model guesses. That is the pathology being
measured.
Scoring is by capability, not tool name. "Which EMEA orders were cancelled?"
is find_orders on one surface and any of three near-synonyms on another.
Grading against a fixed name would score surfaces against each other's
vocabulary instead of against user intent.
The confound, and what's done about it
If the 81-tool surface got auto-generated stubs while the 7 got tuned prose, the experiment would measure granularity plus writing quality and report it as granularity. One interviewer question — "were the descriptions equally good?" — and the chart is worthless. Two defences:
Both generated surfaces compose their descriptions from the same per-operation fragments the task tools are written against, so no surface is handicapped by neglect.
Description quality is a second axis, not an afterthought — which is why this is a 2×2 rather than a line.
What is measured, and what is not
Token overhead is measured and reported above: 4.1× fewer characters of tool
definitions from endpoint/tuned to task/tuned, paid on every request before
the user has said anything.
Note what the table already shows: at tuned quality the endpoint surface costs only 2.3% more than at weak, because 81 short descriptions add up to about what 7 rich ones do. The description axis costs almost nothing at high tool counts and a great deal at low ones — which is not what I expected going in, and is the kind of thing that only shows up when you build the 2×2 instead of the line.
Selection accuracy is not yet measured. It needs a model to act as the client:
export ANTHROPIC_API_KEY=...
npm run eval # 6 configs x 51 requests x 3 trials ≈ 918 calls
npm run eval:reportUntil that runs, there is no accuracy claim here. npm run eval -- --dry-run
produces the token-overhead half with no key.
Task-shaped, not endpoint-shaped
find_orders(query, limit, cursor) absorbs what an endpoint-shaped surface
would spread across list_orders(status, customer_id, from_date, to_date, page, per_page) + get_customer + a date conversion the model performs in its head:
$ find_orders("cancelled EMEA orders since March")
{ "total_matched": 4,
"interpreted_filters": {
"statuses": ["cancelled"], "region": "EMEA",
"date_range": { "from": "2026-03-01T00:00:00.000Z",
"to": "2026-08-02T00:00:00.000Z",
"understood_as": "since march 2026" } } }The complexity did not disappear — it moved from the model's reasoning into
server code, where it is testable. tests/dates.test.ts pins down 32 cases
the model would otherwise get silently wrong: that "in March" asked in August
2026 means March 2026 and not 2027, that "since March" and "in March" are
different ranges, that upper bounds are exclusive.
And it is returned as interpreted_filters, so a misparse is visible in the
result rather than producing a plausible wrong answer.
A cut worth defending: there is no get_customer. Customers are only ever
reached through orders in this domain, so folding the customer fields into
get_order_detail removed a tool without removing a capability.
query_metrics is a deliberate exception — the one endpoint-shaped escape
hatch, because a closed set of task tools cannot answer aggregate questions
nobody anticipated. Being the most capable tool makes it the most dangerous, so
it carries every control above. It also carries the largest description on the
surface (it has to publish the schema), which is the honest cost of an escape
hatch: paid in tokens on every single request.
Running it
npm install
cp .env.example .env
docker compose up -d
npm run migrate && npm run seed
npm run corpus:ingest # corpus/ is committed; --dry-run to chunk only
npm run link:github -- u-ana # per-user GitHub credential (uses `gh auth token`)
npm start # http://localhost:8080/mcpNeither API key is required to run the server. Without OPENAI_API_KEY the
document store indexes lexically and says so in every result
(backend: "lexical") — degrading visibly matters, because retrieval that
quietly gets worse is indistinguishable from a corpus that lacks the answer.
Without ANTHROPIC_API_KEY, search_documents returns passages verbatim
instead of condensing them.
What the embeddings actually bought
The corpus is indexed with text-embedding-3-small (1536 dims, 360 chunks,
HNSW over cosine distance). The model name is pinned in ops.index_meta as well
as in config, so changing EMBED_MODEL without re-ingesting fails loudly instead
of silently comparing vectors from two different encoders.
Both backends were run against the same paraphrased questions — queries that deliberately avoid the corpus's own vocabulary. The honest summary is lexical finds the right document; vector finds the right section:
query | lexical (OR top-up) | vector |
"stop a client replaying elicitation state as another user" |
|
|
"can a shared cache store the tool list" | — |
|
Strict keyword search (websearch_to_tsquery, which ANDs every term) returns
zero rows for all three paraphrases — nine content words never co-occur in one
chunk. That is why the lexical path ANDs first for precision and then tops up
from an OR query for recall, rather than doing either alone.
End to end, with condensation:
$ search_documents("what must a server do to requestState to stop one user
replaying another user's state?")
backend: vector | condensed: true | passages: 6
To prevent one user from replaying another user's state, a server MUST
include the authenticated principal inside the integrity-protected
`requestState` payload and verify it on receipt [2] … [complementary
measures: a short TTL, and an identifier for the originating request]
[1] basic__patterns__mrtr — Security Considerations
[2] basic__patterns__mrtr — Server Requirements (Basic Workflow)
[3] client__elicitation — StatefulnessNote where the condensation happens: before the six passages cross into the conversation. That is the whole point — the context saving is only real if the raw text never arrives.
DEV_AUTH=true (the default in .env.example) accepts unsigned
dev.<base64url({sub,name,scopes})> tokens so the whole surface — including the
per-scope filtering that is most of the point — is exercisable before an Entra
tenant exists. assertDeployable refuses to start a non-localhost server with
it on.
# mint a token and call the server
TOK=$(node -e "console.log('dev.'+Buffer.from(JSON.stringify({
sub:'u-ana', name:'Ana', scopes:['orders:read','metrics:read']})).toString('base64url'))")
curl -s -X POST localhost:8080/mcp \
-H "Authorization: Bearer $TOK" -H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Mcp-Method: tools/list' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{
"io.modelcontextprotocol/protocolVersion":"2026-07-28",
"io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1"},
"io.modelcontextprotocol/clientCapabilities":{}}}}'
Mcp-MethodandMcp-Nameare required headers on 2026-07-28 streamable HTTP POSTs, so gateways can route without parsing the body.tools/callwithoutMcp-Nameis rejected as a malformed message.
Tests
npm test # 94 tests
npm run typecheckThe suites drive a real Client against the real handler in-process —
handler.fetch is the same function the Express route calls, so no port, no
socket and no mock transport is involved. The database-backed suites skip with a
warning when no container is running, so a fresh checkout stays green; CI
provides the service so the skip never hides a regression there.
Stack
Concern | Choice |
Protocol | MCP 2026-07-28, with 2025-era clients served from the same endpoint ( |
SDK |
|
Runtime | Node 22+, TypeScript strict, zod v4 |
Data | Postgres 16 + pgvector |
Auth | OAuth 2.1 + PKCE, Microsoft Entra ID (a free tenant needs no paid subscription) |
On the spec revision.
2026-07-28is the largest MCP revision since launch: the protocol went stateless (noinitialize, no session id), and server-initiated requests were replaced by Multi Round-Trip Requests. Two consequences this project is built around — sampling is now deprecated (so it is not implemented here;search_documentswill condense via the provider API directly, which is the spec's own recommended migration), and an elicitation pause is no longer session state but a signed, principal-bound, TTL'drequestStateblob the client echoes back on retry.
Roadmap
1. Postgres, transport, auth | ✅ streamable HTTP, OAuth resource server, per-caller tool visibility, constrained SQL with EXPLAIN gating |
2. GitHub + documents + the surfaces | ✅ real rate-limited GitHub with per-user credentials, 360-chunk spec corpus, three surfaces from one operations table, harness runs — accuracy pending a key |
3. Elicitation, contracts, CI | MRTR elicitation on the write tools with a signed |
4. Deploy | Azure Container Apps, and the same server demonstrated in Claude Code, Cursor and a custom client with zero per-client integration code |
Whatever the accuracy numbers say when they run — including a smaller effect than expected — they get reported. A measured modest result is worth more than an impressive guess, and an interviewer can tell the difference instantly.
Related
Sibling project: bank-support-agent — a guarded
LangGraph loop with a durable human-approval gate. Deliberately a different
story at a different layer: approval there is an orchestrator-level
interrupt() with a Postgres checkpoint; approval here will be protocol-level
elicitation that needs no orchestrator. Same requirement, two layers of the
stack, and the comparison is the point of having built both.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to query live schema, lineage, and query-context across data warehouses, dbt projects, orchestration systems, and BI tools via MCP tools.Apache 2.0
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to access a unified catalog of tools from various APIs (OpenAPI, GraphQL, MCP, Google Discovery) through the MCP protocol.MIT
- FlicenseNot gradedqualityCmaintenanceEnables natural language interaction with enterprise tools including file, database, GitHub, Slack, browser, calendar, email, vector search, and Python calculation through OpenAI and MCP Client.
- FlicenseNot gradedqualityCmaintenanceEnables an LLM to dynamically discover and call tools across multiple MCP servers (file, GitHub, SQL, Python execution) with authentication, rate limiting, and observability, supporting parallel execution and secure deployment.
Related MCP Connectors
Shared, governed long-term memory for AI agents across tools and sessions via MCP and REST.
Hosted MCP endpoint with realistic fake data for prototyping agents. 12 tools, no setup.
Free public MCP for AI agents — 193 tools, 44 workflows. No API key.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/indraxneel/nexus-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server