WisdomAgent
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., "@WisdomAgentWhy do customers abandon checkout when a discount code is applied?"
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.
WisdomAgent
Grounded agentic Q&A over customer feedback. An analyst asks a question in English; a bounded agent plans, calls typed tools over a feedback corpus, and returns an answer in which every claim cites specific feedback record IDs — or refuses to answer at all.
Table of contents
Related MCP server: tero-mcp-lite
Why this exists
A support inbox, an app store and a CSAT survey add up to tens of thousands of short, messy documents. The questions people want to ask of that pile are exactly the questions an LLM is good at sounding right about:
Why do customers abandon the checkout flow when a discount code is applied?
The failure mode is not that the model says nothing. It is that it says something plausible, specific and unsourced — and it gets quoted in a roadmap review. Once one number in a deck turns out to be invented, every number in every deck is suspect, and the tool is finished.
WisdomAgent's premise is that an unverifiable answer is worse than no answer. So the system is built around a verifier rather than a generator:
Retrieval finds candidate feedback records.
A bounded agent drafts an answer from those records only.
A grounding checker re-derives, claim by claim, whether the draft is actually supported by the evidence — and by which record.
Anything unsupported is pruned. If pruning leaves nothing, the system refuses and says why.
A refusal is a first-class success. It is measured (wa_refusals_total), alerted on in both directions (too many refusals and too few), and scored in the offline eval as refusalCorrectness — because a system can trivially drive its hallucination rate to zero by refusing everything.
What it does
The same domain, surfaced three ways, with no second-class implementation behind any of them:
Surface | Endpoint | For |
GraphQL |
| Analysts and product UIs. One round trip returns the answer, its grounding report, the fused ranking with component scores, the source documents and the agent trace. |
MCP |
| Agent clients. JSON-RPC 2.0 |
Monitors | worker process | Saved questions evaluated on a cadence, emitting deduplicated digest events when a condition breaches. |
Architecture
Hexagonal (ports and adapters). The dependency rule is enforced by import discipline and asserted by a test: wisdomagent/domain/ imports nothing from FastAPI, asyncpg, aiokafka, redis or httpx. All retrieval mathematics, budget accounting, grounding logic and monitor state machines live there and are testable with no infrastructure at all.
flowchart TB
subgraph clients["Clients"]
UI["Analyst / product UI"]
AGENTC["MCP agent client"]
PROM["Prometheus"]
end
subgraph driving["Driving adapters"]
GQL["GraphQL schema<br/><code>api/schema.py</code>"]
MCP["MCP server<br/><code>adapters/mcp/server.py</code>"]
HTTP["FastAPI app + middleware<br/><code>api/app.py</code>"]
WORK["Monitor worker<br/><code>worker.py</code>"]
end
subgraph app["Application layer — orchestration only"]
CMD["FeedbackCommandService<br/>(writes)"]
QRY["FeedbackQueryService<br/>(reads)"]
SAGA["AnswerSaga"]
MED["Mediator + middleware<br/><code>app/messages.py</code>"]
PORTS["Ports (Protocols)<br/><code>app/ports.py</code>"]
end
subgraph domain["Domain — pure, no I/O"]
MODEL["Model & value objects"]
LEX["BM25<br/><code>domain/lexical.py</code>"]
VEC["Embedder + ANN<br/><code>domain/vector.py</code>"]
FUSE["RRF / weighted fusion"]
RANK["Feature reranker"]
GRND["Grounding checker"]
BUD["Budget"]
PLAN["Planner"]
MON["Monitor state machine"]
EVAL["Eval harness"]
PART["Partition / shard maths"]
end
subgraph driven["Driven adapters"]
MEM["In-memory stores"]
PG["Postgres<br/>(partitioned + sharded)"]
KAF["Kafka publisher"]
RED["Redis cache"]
LLM["LLM client<br/>(planner / drafter / judge)"]
TOOLS["Tool catalogue"]
end
subgraph obs["Observability"]
MET["Prometheus registry"]
LOG["Structured JSON logs"]
TRC["OpenTelemetry"]
end
UI --> HTTP --> GQL --> MED --> CMD & QRY
AGENTC --> HTTP --> MCP --> TOOLS
WORK --> CMD
QRY --> SAGA
SAGA --> PLAN & LEX & VEC & FUSE & RANK & GRND & BUD
CMD --> MODEL & MON & EVAL & PART
CMD & QRY & SAGA -.->|depend only on| PORTS
PORTS -.->|implemented by| MEM & PG & KAF & RED & LLM
TOOLS --> QRY
HTTP & SAGA & CMD --> MET & LOG & TRC
PROM --> MET
classDef pure fill:#e8f5e9,stroke:#2e7d32
classDef adapter fill:#e3f2fd,stroke:#1565c0
classDef appc fill:#fff3e0,stroke:#ef6c00
class MODEL,LEX,VEC,FUSE,RANK,GRND,BUD,PLAN,MON,EVAL,PART pure
class GQL,MCP,HTTP,WORK,MEM,PG,KAF,RED,LLM,TOOLS adapter
class CMD,QRY,SAGA,MED,PORTS appcComponent reference
Every box in the diagram, what it owns, and why it is separate from its neighbours.
Component | Module | Responsibility | Why it is its own box |
FastAPI app + middleware | Correlation ids, rate limiting, RED metrics, probes, error→status mapping | Middleware order is a correctness concern: correlation is outermost so every log line and error body carries one; rate limiting sits inside it so a flood is rejected before it can allocate an index scan | |
GraphQL schema | Types, resolvers, argument validation | GraphQL, not REST, because one | |
Mappers | Domain → GraphQL type conversion | Keeps | |
MCP server | JSON-RPC 2.0 envelope, tool schemas, output truncation | Transport concern only. It reuses the exact | |
Monitor worker | Evaluate saved monitors on a cadence, forever | Separate process: a sweep is bursty CPU that would otherwise add its latency to whatever request shared the event loop, and the two scale on different signals | |
| Index, delete, save monitors, run evals; publishes events | CQRS write side. Commands publish events and never return projections | |
| Search, ask, traces, digests, health | CQRS read side. Queries never mutate the corpus | |
| plan → retrieve → rerank → draft → verify → repair → finalize | The interesting failure here is not a crash but a draft that cannot be grounded; the saga's compensation is a downgrade, not a rollback | |
Mediator | Message dispatch with pluggable middleware | Logging, timing and correlation apply to every use case without each one repeating them | |
Ports |
| Runtime-checkable structural typing: an adapter is compatible because of its shape, not because it inherited from us. A test fails any port that grows past 8 methods, which is what forced | |
Model & value objects |
| Frozen and validating. An invalid | |
BM25 | Inverted index, IDF, length normalisation | Written from scratch: an exact-term match on "SAVE20" is what a lexical arm exists for, and it must be inspectable to be arguable | |
Embedder + ANN | Deterministic hashed embeddings, flat and IVF-lite search | Deterministic by design — the offline path must produce the same ranking on every machine, or the eval harness is measuring the weather | |
Fusion | Reciprocal rank fusion and weighted-score fusion | RRF needs no score calibration between two arms whose scores are on incomparable scales; weighted fusion is available when they are calibrated | |
Reranker | Explainable feature scoring (relevance, recency, source, coverage) | Every feature contribution is returned, so "why is this document first" has an answer that is not "the model felt like it" | |
Grounding checker | Per-claim support, best span, citation precision, coverage | The component the entire product rests on. Deliberately independent of the drafter, so a drafter bug cannot mark its own homework | |
Budget | Step, per-tool, token and wall-clock ceilings | What makes "agentic" safe to run in production: an unbounded loop is an unbounded bill and an unbounded latency | |
Planner | Intent classification → tool plan | Deterministic heuristic by default; the LLM planner is an optional upgrade that falls back to this one | |
Monitor state machine | ok → pending → firing → resolved, breaches, cooldown, fingerprints | Anti-flap logic is pure and unit-tested; the scheduler around it is trivial | |
Eval harness | Case grading, aggregation, score gate | The regression gate on the answer path. Scores refusals for correctness, not just for absence | |
Partition maths | Tenant→shard hashing, time→partition mapping, balance | The same function decides the shard in Python and in SQL, so the app and the schema can never disagree | |
In-memory stores | Repository, index, traces, monitors, evals, publisher, cache | Not test doubles — the default production adapters for a single-node deployment, which is why they are held to the same coverage bar | |
Postgres | Partitioned, sharded document storage | Optional. Blank DSN ⇒ the in-memory repository, and every test still passes | |
Kafka publisher | Domain events to a topic | Optional. Blank brokers ⇒ in-process publication | |
Redis cache | Answer/aggregate caching with TTL | Optional. Blank URL ⇒ no cache; a cache is never load-bearing for correctness | |
LLM client | Chat completions behind the full resilience stack | Optional. Blank base URL ⇒ the deterministic offline path, everywhere | |
Tool catalogue | The four typed tools and their schemas | Shared by the internal agent and the MCP surface; returns a failed | |
Metrics | 33 | Private registry, not the global default: two containers in one process must not fight over collector names | |
Logging | Structured JSON, correlation propagation via | Correlation survives |
Design patterns in use
Named, and each with the specific problem it solves here.
Pattern | Where | Problem it solves |
Hexagonal / Ports & Adapters |
| The domain is testable without Postgres, Kafka, Redis or an LLM — which is why the suite runs in ~3 s |
CQRS |
| Reads outnumber writes by orders of magnitude and want different caching and consistency |
Saga (with compensation) |
| A multi-step answer where the interesting failure is semantic; compensation is pruning and refusal, not rollback |
Strategy |
| Swap an algorithm by configuration without touching the caller |
Adapter |
| Third-party clients never leak past the port boundary |
Repository |
| Persistence is an implementation detail of storage, not a shape the domain has to know |
Mediator |
| Cross-cutting middleware (logging, timing, correlation) applied once instead of per use case |
Decorator / Middleware |
| Behaviour composed around a call without editing the call |
Circuit Breaker + Bulkhead + Retry + Timeout + Rate Limiter |
| Every I/O boundary; see Resilience |
Factory |
| One place decides which adapters a deployment gets, based on configuration |
Builder |
| Frozen configuration assembled from the environment, validated once at startup |
Value Object |
| Illegal states unrepresentable; validation happens at construction, once |
State machine |
| Explicit |
Template Method |
| Fixed order — bulkhead, breaker, timeout, retry — with the operation as the varying part |
Outbox |
| An event and the state change that caused it commit together or not at all |
Null Object | in-memory cache/publisher when Redis/Kafka are unconfigured | Absence of infrastructure is not a branch in business logic |
Specification |
| Query predicates composed and pushed into retrieval rather than applied to the top-k afterwards |
Core flows
1. Ingest
sequenceDiagram
autonumber
participant C as Client
participant G as GraphQL
participant CM as CommandService
participant R as Repository
participant IX as HybridSearchIndex
participant EV as EventPublisher
C->>G: mutation indexDocuments(documents, batchId)
G->>CM: index(tenant, documents, batchId)
CM->>CM: validate each DocumentInput → Document
Note over CM: Invalid source / blank text are rejected<br/>per document, not per batch
CM->>R: save_many(tenant, documents)
R-->>CM: newly saved (existing ids skipped)
CM->>IX: add(documents)
IX->>IX: tokenize → BM25 postings
IX->>IX: embed → ANN insert
CM->>EV: documents.indexed
CM-->>G: IndexResult{saved, indexed, corpusSize}Idempotent per document id, so an at-least-once delivery pipeline can replay a batch safely. saved and indexed are returned separately precisely so a replay is visible as 0 / 0 rather than silently swallowed.
2. Ask — the answer saga
sequenceDiagram
autonumber
participant C as Client
participant Q as QueryService
participant S as AnswerSaga
participant P as Planner
participant IX as HybridSearchIndex
participant RK as Reranker
participant D as Drafter
participant V as GroundingChecker
participant T as TraceStore
C->>Q: ask(question, topK, filters)
Q->>S: run(query, budget)
S->>P: classify intent → plan
S->>IX: search(query, limit = topK × multiplier)
IX-->>S: candidate hits (fused)
S->>RK: rerank(hits, documents, now)
RK-->>S: ordered documents + feature breakdown
alt no documents
S-->>Q: REFUSE "no indexed feedback matched"
else evidence relevance < floor
Note over S: Ranking has no notion of "nothing here is relevant".<br/>Without this gate, an extractive draft would quote the<br/>nearest documents and the verifier would pass it —<br/>a quote is trivially supported by its own source.
S-->>Q: REFUSE "does not discuss this topic"
else
S->>D: draft(query, documents)
D-->>S: Answer{claims[], citations[]}
S->>V: verify(claims, evidence)
V-->>S: GroundingReport{per-claim support}
alt accepted
S->>T: save trace
S-->>Q: AskResult
else repairable
S->>D: repair — prune to supported claims
S->>V: re-verify
S-->>Q: AskResult (downgraded) or REFUSE
end
endEvery step is appended to the trace before it can fail, so a partial run is still a readable audit record — and every appended step is also counted into wa_saga_steps_total, so the Grafana step-mix panel and the audit trail are the same story at two resolutions (there is a test asserting exactly that).
The budget is checked at every step: steps used, tool calls per tool, tokens and wall clock. Exceeding any of them ends the run with a typed error rather than an unbounded loop.
3. Hybrid retrieval, fusion and reranking
flowchart LR
Q["Query text"] --> TOK["Tokenize<br/>fold, strip, n-grams"]
TOK --> BM["BM25<br/>inverted index"]
Q --> EMB["Hashed embedder<br/>deterministic, d=128"]
EMB --> ANN["ANN index<br/>flat / IVF-lite"]
BM --> L["Lexical ranking"]
ANN --> V["Vector ranking"]
L --> F{"Fusion<br/>strategy"}
V --> F
F -->|rrf| RRF["Reciprocal rank fusion<br/>1/(k + rank)"]
F -->|weighted| W["Weighted score<br/>λ·lex + (1−λ)·vec"]
RRF --> CAND["Candidates<br/>topK × multiplier"]
W --> CAND
CAND --> RR["Feature reranker"]
RR --> FEAT["relevance · recency<br/>source weight · coverage"]
FEAT --> TOPK["Final top-k<br/>+ per-feature explanation"]Both component scores survive into the API response. That is what makes a ranking argument settleable: a result with a high vector score and a zero lexical score is a semantic match with no term overlap, and that is usually where a surprising citation comes from.
RRF is the default because the two arms produce scores on scales that were never calibrated against each other — BM25 is unbounded and corpus-dependent, cosine similarity is in [−1, 1]. Rank fusion sidesteps calibration entirely. Weighted fusion is there for deployments that have done the calibration work.
4. Grounding verification and refusal
flowchart TD
A["Draft answer"] --> B["Split into claims"]
B --> C{"For each claim"}
C --> D["Find the best supporting span<br/>across the retrieved evidence"]
D --> E{"support ≥ min_support?"}
E -->|yes| F["Supported — record docIds + span"]
E -->|no| G["Unsupported — record reason"]
F & G --> H["GroundingReport<br/>groundedness · citationPrecision · coverage"]
H --> I{"groundedness ≥ min_groundedness<br/>and every claim supported?"}
I -->|yes| J["ACCEPT — return with citations"]
I -->|no| K{"repairs remaining?"}
K -->|yes| L["Prune unsupported claims<br/>re-verify the remainder"]
L --> I
K -->|no| M["REFUSE<br/>with the failure reason"]
J --> N["wa_groundedness_score<br/>wa_citation_precision"]
M --> O["wa_refusals_total{reason}<br/>wa_unsupported_claims_total"]Three distinct gates, each catching a different lie:
Evidence relevance floor — is the retrieved evidence about the question at all? Catches "confidently answering from the nearest unrelated documents".
Per-claim support — is this sentence traceable to that record? Catches a fluent summary that quietly generalises beyond its sources.
Citation precision — does every cited id actually support the claim it is attached to? Catches citation padding, where the answer is right but the sourcing is decorative.
5. MCP tools/call
sequenceDiagram
autonumber
participant M as MCP client
participant H as POST /mcp
participant S as McpServer
participant TC as ToolCatalogue
participant Q as QueryService
M->>H: {"jsonrpc":"2.0","id":1,"method":"initialize"}
H->>S: handle(request)
S-->>M: serverInfo + capabilities
M->>H: tools/list
S-->>M: 4 tools with JSON Schema
M->>H: tools/call {name, arguments}
S->>S: strip reserved tenant args (defence in depth)
S->>TC: execute(name, tenant, args) with timeout
TC->>Q: typed query
alt tool succeeded
TC-->>S: ToolResult{ok, data}
S->>S: truncate to WA_MCP_MAX_OUTPUT_CHARS
S-->>M: {"result":{"content":[…],"isError":false}}
else tool failed (bad argument, unknown tool)
TC-->>S: ToolResult{ok:false, error}
S-->>M: {"result":{"content":[reason],"isError":true}}
else malformed request (no tool name)
S-->>M: {"error":{"code":-32602,…}}
endThe distinction in the last two branches is deliberate. A failed tool comes back as a normal result with isError: true, because the model can read the reason and re-plan. A malformed request goes into the JSON-RPC error channel, because there is nothing to re-plan against. Collapsing the two would either abort a recoverable turn or hide a client bug.
Tenant arguments are stripped from arguments before dispatch even though ToolCatalogue.execute takes the tenant separately and ignores the mapping — defence in depth, so a future tool cannot accidentally honour a caller-supplied tenant.
6. Monitor fires, digest published
stateDiagram-v2
[*] --> ok
ok --> pending: condition breached
pending --> pending: breaches < consecutiveBreaches
pending --> firing: breaches reached
pending --> ok: condition cleared
firing --> firing: still breached, within cooldown (no new digest)
firing --> resolved: condition cleared
resolved --> ok: acknowledged / next sweep
firing --> [*]: monitor deletedsequenceDiagram
autonumber
participant W as Worker
participant CM as CommandService
participant MS as MonitorStore
participant Q as QueryService
participant EV as EventPublisher
loop every WA_MONITOR_INTERVAL_SECONDS
W->>MS: tenants()
loop per tenant (isolated)
W->>CM: evaluate_monitors(tenant)
CM->>MS: monitors(tenant)
CM->>Q: run each saved query
CM->>CM: domain state machine → transition
alt transition to firing and outside cooldown
CM->>CM: build Digest with supporting docIds
CM->>CM: fingerprint → dedupe
CM->>EV: digest.published
end
end
endPer-tenant isolation is explicit: one tenant's broken monitor must not stop every other tenant's alerts from being evaluated, and a sweep that throws is logged and followed by the next sweep. A worker that exits on a transient database blip is worse than one that skips a cycle.
Digests carry the docIds that triggered them. An alert that says "billing complaints are up" without the records is the same unverifiable claim the whole system exists to prevent.
7. The eval harness
flowchart LR
ES["EvalSet<br/>cases with expected<br/>docIds / refusal"] --> RUN["runEval"]
RUN --> ASK["ask() per case"]
ASK --> GRADE["grade_case"]
GRADE --> M1["exactRate"]
GRADE --> M2["hallucinationRate"]
GRADE --> M3["refusalCorrectness"]
GRADE --> M4["citationPrecision / recall"]
GRADE --> M5["groundedness"]
GRADE --> M6["p50 / p99 latency"]
M1 & M2 & M3 & M4 & M5 & M6 --> AGG["aggregate"]
AGG --> GATE{"ScoreGate"}
GATE -->|pass| OK["passed: true"]
GATE -->|fail| BAD["passed: false<br/>+ named failing cases"]
OK & BAD --> MET["wa_eval_runs_total{result}<br/>wa_eval_score{metric}"]refusalCorrectness is the metric that keeps the rest honest. Refusals are scored against cases that should be refused, so the trivial way to win on hallucinationRate — refuse everything — loses badly here.
Graceful degradation
Nothing outside the process is required. Each dependency degrades to a deterministic in-process path, and the full test suite runs on the degraded path, which is what stops it from bit-rotting.
Dependency | Configured with | Absent ⇒ | Consequence |
Postgres |
| In-memory repository | Single node, no durability across restarts. Same port, same tests |
Kafka |
| In-process publisher | Events still fan out to subscribers; nothing leaves the process |
Redis |
| No cache | Slower repeats. A cache is never load-bearing for correctness |
LLM |
| Heuristic planner + extractive drafter + rubric judge | Answers are extractive quotes rather than prose. Every citation guarantee is unchanged |
OTLP collector |
| No-op tracer | Metrics and logs are unaffected |
Degradation is also dynamic, not just a startup decision: when the LLM is configured but its circuit breaker opens, the saga falls back to the offline drafter mid-flight and reports which one produced the answer (answer.model — see the real output below, where it reads "extractive").
The design consequence worth stating plainly: the offline path is not a stub. Retrieval, fusion, reranking, grounding verification, refusal, monitors and evals are all fully implemented in pure Python. The LLM upgrades fluency, not correctness.
Data model, partitioning and sharding
See db/migrations/0001_init.sql.
erDiagram
DOCUMENTS ||--o{ AGENT_TRACES : cited_by
MONITORS ||--o{ MONITOR_STATUS : has
MONITORS ||--o{ DIGESTS : emits
EVAL_SETS ||--o{ EVAL_REPORTS : scored_by
DOCUMENTS {
text doc_id PK
text tenant_id
smallint shard
text source
text theme_id
timestamptz occurred_at PK
}
AGENT_TRACES {
text run_id PK
text tenant_id
timestamptz created_at PK
bool refused
jsonb steps
}
MONITORS {
text monitor_id PK
text tenant_id
text query
jsonb condition
}
DIGESTS {
text digest_id PK
text monitor_id FK
text dedupe_key
jsonb doc_ids
}Two orthogonal axes, because they answer different questions:
Range partitioning by
occurred_at(monthly). Feedback analysis is overwhelmingly recent-window: "what changed this quarter". Monthly partitions let the planner prune everything else, and retention becomesDROP PARTITIONrather than aDELETEthat bloats the table.Hash sharding by tenant into
WA_TENANT_SHARDSbuckets. Prevents one large customer from turning into a hot partition. The shard is computed bydomain/partition.pyand stored as a column, so the same function decides placement in the application and in SQL — they cannot disagree.
/ready reports shard_balance. On a single-tenant demo it reads 0.25 with four shards, which is correct and expected: one tenant occupies exactly one bucket. A multi-tenant deployment where it drifts toward 0 is the hot-tenant failure mode, and there is an alert for it.
The composite primary key is (doc_id, occurred_at) because Postgres requires the partition key to be part of every unique constraint.
Resilience
Every outbound I/O call goes through one ResiliencePolicy[T], composed in a fixed order:
flowchart LR
CALL["Call"] --> BH["Bulkhead<br/>bounded concurrency"]
BH --> CB{"Circuit breaker"}
CB -->|open| FAST["Fail fast<br/>→ fallback"]
CB -->|closed / half-open| TO["Timeout"]
TO --> RETRY["Retry<br/>exponential backoff + jitter"]
RETRY --> OP["Operation"]
OP -->|failure| CB
OP -->|success| DONE["Result"]The order is not arbitrary:
Bulkhead outermost — the concurrency cap has to apply to waiting callers too, or a slow dependency backs up unbounded queues of tasks that are all going to time out anyway.
Breaker before timeout — when a dependency is known-dead, failing fast is strictly better than waiting the full timeout to learn it again.
Timeout inside retry — each attempt gets its own deadline; a retry that inherits an already-expired budget is a wasted call.
Jitter always — synchronised retries from many replicas are how a recovering dependency gets knocked back down.
Applied to: Postgres queries, Redis get/set, Kafka publishes, LLM completions, and every MCP tool dispatch. Breaker state is exported per dependency as wa_circuit_breaker_state, and both CircuitBreakerOpen and RetryStorm are alerting rules — with an inhibition so an open breaker suppresses the retry-storm page it necessarily causes.
Getting started
Local, no infrastructure
git clone https://github.com/ABHIJEET-MUNESHWAR/wisdomagent.git
cd wisdomagent
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# tests, lint, benchmark — none of these need a container
python -m pytest -q
python -m ruff check . && python -m ruff format --check .
python -m benchmarks.bench_retrieval
# run it, seeded with a synthetic corpus
WA_BOOTSTRAP_DEMO=true python -m wisdomagent.main
# → http://localhost:8000/graphqlFull stack
docker compose up --buildService | URL | Notes |
API | GraphiQL in the browser | |
MCP | JSON-RPC 2.0 | |
Prometheus | 27 alert rules loaded | |
Grafana |
| |
Alertmanager | Routing tree, 4 inhibition rules | |
Jaeger | Traces via the OTLP collector | |
Worker metrics | The worker's only HTTP surface |
The compose stack additionally starts Postgres (with the migration applied on first boot), Redis and Kafka, so the partitioned schema, the answer cache and the event bus are all exercised for real rather than stubbed.
Using it
GraphQL
curl -sS localhost:8000/graphql \
-H 'content-type: application/json' \
-H 'x-tenant-id: acme' \
-d '{"query":"query { ask(question: \"why are customers unhappy with billing\", topK: 6) { answer { text refused confidence model citedDocuments claims { text citations { docId } } } grounding { groundedness citationPrecision coverage accepted } hits { docId score lexicalScore vectorScore rank } } }"}'Real response from a demo-seeded instance (200 synthetic documents, offline path, abridged):
{
"answer": {
"text": "Billing page is slow and the payment method dropdown fails to load (report 46) Still happening. Billing page is slow and the payment method dropdown fails to load (report 81) Third time this week. Billing page is slow and the payment method dropdown fails to load (report 76) ...",
"refused": false,
"confidence": 0.75,
"model": "extractive",
"citedDocuments": ["doc-00046", "doc-00081", "doc-00076", "doc-00086", "doc-00051", "doc-00016"],
"claims": [
{
"text": "Billing page is slow and the payment method dropdown fails to load (report 46)",
"citations": [{ "docId": "doc-00046" }]
},
{
"text": "Still happening. Billing page is slow and the payment method dropdown fails to load (report 81)",
"citations": [{ "docId": "doc-00081" }]
}
]
},
"grounding": {
"groundedness": 1.0,
"citationPrecision": 1.0,
"coverage": 1.0,
"accepted": true
},
"hits": [
{ "docId": "doc-00046", "score": 1.221095, "lexicalScore": 3.525865, "vectorScore": 0.189384, "rank": 1 },
{ "docId": "doc-00081", "score": 1.100036, "lexicalScore": 3.269024, "vectorScore": 0.187094, "rank": 2 },
{ "docId": "doc-00076", "score": 1.071927, "lexicalScore": 3.154142, "vectorScore": 0.139294, "rank": 3 },
{ "docId": "doc-00086", "score": 1.056670, "lexicalScore": 3.154142, "vectorScore": 0.138375, "rank": 4 }
]
}"model": "extractive" is the offline drafter: no LLM was configured, so claims are verbatim spans. The citation guarantee is identical either way — only the prose changes.
A refusal
Ask something the corpus cannot support and the system says so, with the reason and the gate that stopped it. Real response:
{
"answer": {
"text": "I cannot answer that from the indexed feedback.",
"refused": true,
"refusalReason": "the retrieved feedback does not discuss this topic (relevance 0.12)",
"confidence": 0.0
},
"grounding": {
"groundedness": 0.0,
"accepted": false,
"failureReason": "the draft contained no verifiable claims"
}
}Note that this is HTTP 200 with no errors member. A refusal is a successful outcome, not a fault.
MCP (JSON-RPC 2.0)
curl -sS localhost:8000/mcp \
-H 'content-type: application/json' -H 'x-tenant-id: acme' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"aggregate_by_theme","arguments":{"query":"billing","limit":4}}}'Real response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\"query\": \"billing\", \"themes\": [{\"theme_id\": \"billing\", \"count\": 35, \"doc_ids\": [\"doc-00081\", \"doc-00086\", \"doc-00046\", \"doc-00051\", \"doc-00076\"]}, {\"theme_id\": \"reliability\", \"count\": 21, \"doc_ids\": [\"doc-00050\", \"doc-00055\", \"doc-00130\", \"doc-00185\", \"doc-00105\"]}, {\"theme_id\": \"onboarding\", \"count\": 19, \"doc_ids\": [\"doc-00093\", \"doc-00058\", \"doc-00178\", \"doc-00053\", \"doc-00048\"]}, {\"theme_id\": \"performance\", \"count\": 15, \"doc_ids\": [\"doc-00184\", \"doc-00139\", \"doc-00074\", \"doc-00049\", \"doc-00039\"]}]}"
}
],
"isError": false
}
}Every aggregate carries doc_ids. A count without its records is exactly the kind of number that ends up in a slide deck and cannot be checked.
The four tools: search_feedback, aggregate_by_theme, trend_over_time, get_account_context.
Ops endpoints
curl -sS localhost:8000/readyReal response:
{
"status": "ready",
"corpus": "ready",
"documents": 200,
"vocabulary": 349,
"vectors": 200,
"traces": 2,
"shard_balance": 0.25,
"fusion_strategy": "rrf"
}/health is liveness only and touches no dependency. /ready is the strictly stronger claim a rollout should gate on — and note that an empty corpus is still ready, because a service that correctly refuses every question is working as designed; reporting not-ready there would block the rollout of a freshly provisioned tenant.
Postman
postman/WisdomAgent.postman_collection.json — import and run top to bottom. It covers every GraphQL query and mutation, the MCP handshake, all four tools/calls, batching, all four JSON-RPC error codes, the ops endpoints, and instructions for the WebSocket subscription. Requests capture runId / docId into collection variables, so nothing needs hand-editing.
Every GraphQL document in the collection is validated against the live schema, and every request has been executed against the real application.
Configuration
All settings are WA_-prefixed and parsed once at startup into a frozen Settings. An unparseable value is a ConfigError at boot, not a TypeError under load.
Variable | Default | Notes |
|
| |
|
| Label on every log line and metric |
|
| |
|
| Used when |
|
| Hash-shard count |
|
| Range-partition width |
| (blank) | Blank ⇒ in-memory repository |
| (blank) / | Blank ⇒ in-process publisher |
| (blank) / | Blank ⇒ no cache |
| (blank) / (blank) / | Blank ⇒ deterministic offline path |
| (blank) | Blank ⇒ no-op tracer |
|
|
|
|
| Determinism knobs |
|
| Character n-grams; RRF constant |
|
| |
|
| Oversized requests are clamped, not rejected |
|
| Retrieval depth before reranking |
|
| Agent budget ceilings |
|
| |
|
| |
|
| Verification thresholds |
|
| The refusal floor |
|
| What bounds the grounding cost |
|
| Resilience |
|
| |
|
| |
|
| |
|
| Worker sweep cadence |
|
| Worker's only HTTP surface |
|
| |
| (unset) | Seeds a synthetic corpus and eval set |
Settings.describe() is the safe-to-log view. It never includes the API key.
Observability
Structured JSON logs. One object per line, with a correlation id propagated through contextvars so it survives await boundaries without being threaded through every signature. Accepted from x-correlation-id and echoed back on every response.
Metrics — 33 collectors on a private CollectorRegistry, exposed at /metrics:
RED —
wa_requests_total{operation,outcome},wa_request_duration_seconds{operation}.operationis the route template, never the concrete path: labelling with/document/doc-00042would mint a new time series per document and eventually kill the metrics backend.USE —
wa_bulkhead_in_flight{dependency},wa_circuit_breaker_state{dependency},wa_rate_limited_total{operation},wa_retries_total{dependency},wa_corpus_documents,wa_corpus_terms.Agent SLIs —
wa_groundedness_score,wa_citation_precision,wa_unsupported_claims_total,wa_refusals_total{reason},wa_repairs_total{outcome},wa_agent_steps,wa_agent_tokens,wa_tool_calls_total{tool,outcome},wa_tool_duration_seconds{tool},wa_saga_steps_total{step,outcome},wa_questions_total{outcome}.Domain —
wa_documents_indexed_total,wa_documents_rejected_total{reason},wa_searches_total{strategy},wa_search_hits,wa_eval_score{metric},wa_monitors_evaluated_total,wa_monitor_transitions_total{transition},wa_digests_published_total,wa_mcp_calls_total{method,outcome},wa_events_published_total{event_type,outcome},wa_cache_total{result}.
Alerts — monitoring/alerts.yml, 27 rules in 6 groups, every one with severity, component, summary, description and a runbook annotation. Every rule name is prefixed WisdomAgent… (elided below for width):
Group | Rules |
|
|
|
|
|
|
|
|
|
|
|
|
Alertmanager routes critical → on-call and warning → a lower-urgency channel, with four inhibition rules so one root cause pages once: ApiDown suppresses everything else, CorpusEmpty suppresses the refusal/empty-search/groundedness alerts it necessarily causes, and CircuitBreakerOpen suppresses RetryStorm and RepairsFailing.
Receiver URLs use url_file rather than ${ENV}, because Alertmanager does not expand environment variables in its config — a literal url: ${SLACK_WEBHOOK_URL} fails validation with unsupported scheme "". See monitoring/secrets/README.md.
All of this is machine-validated in CI with promtool check rules, promtool check config and amtool check-config, so a typo in a PromQL expression fails the build instead of failing silently during an incident.
Dashboards — two provisioned Grafana boards: WisdomAgent / Service (RED and USE) and WisdomAgent / Grounding and Agent SLIs (groundedness, hallucination rate, citation precision, refusal reasons, repair outcomes, agent step mix, eval scores and monitor activity).
Tracing — OTLP to the collector, with tail sampling that keeps 100% of errors, slow spans (>5 s), refusals and ungrounded answers, and 10% of everything else. The interesting traces are exactly the rare ones; uniform sampling throws them away.
Complexity
N = corpus size, L = mean document length in tokens, d = embedding dimension (128), k = candidate depth, C = claims, E = evidence documents, F = rerank features, T = tokens per evidence document.
Operation | Time | Space | Notes |
Tokenize corpus | O(N·L) | O(N·L) | Single pass, fold + strip + n-grams |
BM25 index build | O(N·L) | O(V + postings) |
|
BM25 query | O(|q|·postings) | O(k) | Sub-linear in |
Embed batch | O(N·L·d/64) | O(N·d) | Hashed features, no model call |
ANN index build | O(N·d) | O(N·d) | float32 matrix, contiguous |
ANN query (flat) | O(N·d) | O(N) | One matvec |
ANN query (IVF-lite) | O(probes·N/cells·d) | O(N) | Cell centroids + inverted lists |
RRF fusion | O(k log k) | O(k) | Sort dominates |
Weighted fusion | O(k log k) | O(k) | Sort dominates |
Feature rerank | O(k·F) | O(k) |
|
Grounding check | O(C·E·T) | O(C·E) | The verification cost, bounded by |
Full | O(N + k log k) | O(N·d) | Retrieval dominates; everything after it is bounded by |
Shard resolution | O(1) | O(1) | Hash |
Partition resolution | O(1) | O(1) | Date arithmetic |
Monitor sweep | O(M·ask) | O(M) |
|
Benchmarks
benchmarks/bench_retrieval.py measures 11 kernels at four corpus sizes and fits a scaling exponent to log(time) vs log(size) by least squares, then compares it against the documented claim. --check fails the build when a kernel exceeds its limit, which catches an accidental O(N²) that a unit test — which only ever sees a handful of documents — cannot.
It also reports NOISE rather than a verdict when a kernel's timings are below a 0.05 ms floor or when the input size does not actually vary, because grading a constant-time measurement produces an exponent of 0.00 and a meaningless PASS.
Real output, python -m benchmarks.bench_retrieval (Python 3.12, WSL2 on x86-64, GC disabled during measurement, p50/p95 over repeated samples):
kernel docs items mean ms p95 ms us/item items/s
------------------------------------------------------------------------------
tokenize_corpus 500 500 5.371 5.751 10.743 93,085
tokenize_corpus 1000 1000 10.444 10.974 10.444 95,750
tokenize_corpus 2000 2000 20.899 22.336 10.449 95,700
tokenize_corpus 4000 4000 43.262 44.291 10.815 92,460
bm25_build 500 500 7.209 7.926 14.418 69,359
bm25_build 1000 1000 14.761 16.368 14.761 67,746
bm25_build 2000 2000 27.685 28.535 13.842 72,242
bm25_build 4000 4000 60.504 63.112 15.126 66,111
bm25_query 500 500 0.136 0.155 0.272 3,670,883
bm25_query 1000 1000 0.178 0.186 0.178 5,608,273
bm25_query 2000 2000 0.225 0.254 0.113 8,871,532
bm25_query 4000 4000 0.350 0.362 0.087 11,437,813
embed_batch 500 500 53.876 56.313 107.751 9,281
embed_batch 1000 1000 108.840 111.994 108.840 9,188
embed_batch 2000 2000 216.628 218.959 108.314 9,232
embed_batch 4000 4000 456.180 466.971 114.045 8,768
ann_build 500 500 1.770 1.867 3.540 282,524
ann_build 1000 1000 3.684 4.424 3.684 271,425
ann_build 2000 2000 8.091 9.275 4.046 247,182
ann_build 4000 4000 15.837 16.645 3.959 252,569
ann_query 500 500 0.425 1.325 0.850 1,175,898
ann_query 1000 1000 0.188 0.199 0.188 5,333,100
ann_query 2000 2000 0.242 0.286 0.121 8,280,888
ann_query 4000 4000 0.248 0.292 0.062 16,129,878
rrf_fuse 500 50 0.181 0.250 3.626 275,779
rrf_fuse 1000 100 0.084 0.101 0.837 1,195,183
rrf_fuse 2000 200 0.191 0.301 0.956 1,046,010
rrf_fuse 4000 400 0.317 0.372 0.793 1,261,713
weighted_fuse 500 50 0.106 0.119 2.115 472,812
weighted_fuse 1000 100 0.084 0.088 0.837 1,194,963
weighted_fuse 2000 200 0.184 0.188 0.920 1,087,325
weighted_fuse 4000 400 0.369 0.414 0.921 1,085,319
rerank 500 50 0.779 1.095 15.588 64,150
rerank 1000 100 1.356 1.404 13.557 73,763
rerank 2000 200 2.845 3.112 14.225 70,301
rerank 4000 400 5.475 5.885 13.689 73,053
grounding_check 500 8 0.120 0.124 14.980 66,756
grounding_check 1000 10 0.121 0.131 12.066 82,874
grounding_check 2000 20 0.126 0.137 6.290 158,990
grounding_check 4000 40 0.114 0.117 2.839 352,288
agent_ask 500 500 3.089 4.207 6.178 161,863
agent_ask 1000 1000 2.788 3.105 2.788 358,636
agent_ask 2000 2000 3.141 3.382 1.571 636,726
agent_ask 4000 4000 3.039 3.191 0.760 1,316,299
kernel claim exponent limit verdict
-------------------------------------------------------------------------------------
tokenize_corpus O(N * L) 1.00 1.35 PASS
bm25_build O(N * L) 1.01 1.40 PASS
bm25_query O(|q| * postings) 0.44 1.35 PASS
embed_batch O(N * L) 1.02 1.35 PASS
ann_build O(N * d) 1.06 1.45 PASS
ann_query O(N * d) flat, O(probes * N/cells * d) IVF -0.20 1.35 PASS
rrf_fuse O(k log k) 0.36 1.30 PASS
weighted_fuse O(k log k) 0.65 1.30 PASS
rerank O(k * F) 0.95 1.30 PASS
grounding_check O(C * E * T) -0.03 1.30 PASS
agent_ask O(N + k log k) per run 0.01 1.40 PASSReading these honestly:
bm25_queryat 0.44 andann_queryat −0.20 are sub-linear because the measured work does not scale withNthe way the worst case does — postings for a discriminative term stay short, and the ANN mat-vec is memory-bandwidth-bound long before it is compute-bound at these sizes.grounding_checkat −0.03 is flat because it is bounded byWA_MAX_CLAIMSandWA_EVIDENCE_LIMIT, not by corpus size. That is the design working: verification cost does not grow with the corpus.agent_askat 0.01 for an end-to-end run is the same effect — retrieval is the onlyN-dependent term and it is cheap relative to the fixed pipeline cost at these sizes.The fusion exponents are the least trustworthy numbers here.
rrf_fusefits 0.36 in this run and 1.02 in the previous one, purely because its smallest data point (0.181 ms mean, 0.250 ms p95 at 500 documents) caught an outlier and dragged the fit. Both are well inside the 1.30 limit and both are consistent with theO(k log k)claim, but a least-squares fit over four points near the noise floor is a weak instrument, and I would not quote either figure to two decimal places.
That last bullet is the honest caveat on the whole method: the exponents are a regression detector, not a measurement. They reliably catch the 1.6-vs-1.0 kind of change that a cache cliff or an accidental nested loop produces. They do not resolve 0.36 from 1.02 on a kernel that runs in a third of a millisecond, which is exactly why the harness reports NOISE instead of a verdict when a kernel never leaves the 0.05 ms floor.
Absolute timings are machine- and load-dependent and should not be compared across runs on different hardware.
A real bug this found
ann_query originally failed its gate. The kernel measured 0.16 ms at 2 000 documents and 3.4 ms at 4 000 — a discontinuous jump, not a growth curve, which is the signature of a cache cliff rather than an algorithmic problem. Profiling put all of it inside the matrix @ query mat-vec.
Two causes, compounding. For a tall-and-skinny mat-vec, OpenBLAS's dgemv spends more time coordinating threads than multiplying; and the float64 matrix crosses out of cache somewhere past 2 000 rows, so the cost jumps rather than growing smoothly.
The fix in wisdomagent/domain/vector.py was to materialise the index as a contiguous float32 matrix (half the bytes ⇒ twice as many vectors resident in cache) and to compute similarities with np.einsum("ij,j->i", …) instead of @, sidestepping the BLAS gemv path entirely. Post-fix, the same kernel is the flat 0.425 / 0.188 / 0.242 / 0.248 ms row in the table above — an order of magnitude faster at 4 000 documents, and no longer growing.
This is the argument for having a complexity gate at all. Every unit test passed before and after; nothing was functionally wrong. The suite runs on tens of documents, where the cliff is invisible.
Test suite
893 tests, 91% line coverage, no infrastructure required — the whole suite runs against in-memory adapters and an injectable FixedClock, in about 3 seconds.
Real output:
$ python -m ruff check . && python -m ruff format --check .
All checks passed!
87 files already formatted
$ python -m pytest -q
893 passed, 1 warning in 3.85s$ python -m pytest --cov=wisdomagent --cov-report=term-missing -q | tail -30
wisdomagent/app/messages.py 138 10 93% 227-228, 231-232, 239-240, 243-244, 247-248
wisdomagent/app/ports.py 41 3 93% 163-166
wisdomagent/app/service.py 424 56 87% 217-218, 278-282, 320-322, 376, 378, 408-410, 464, 507, 533-561, 579, 621-643, 674-676, 732, 779, 843, 848-856, 860
wisdomagent/config/__init__.py 2 0 100%
wisdomagent/config/settings.py 195 23 88% 116, 118, 122, 126, 160, 162, 170, 172, 174, 176, 325, 329, 331, 335, 339, 341, 343, 345, 347, 349, 351, 353, 355
wisdomagent/container.py 118 11 91% 222, 237, 253, 260-272
wisdomagent/domain/__init__.py 14 0 100%
wisdomagent/domain/budget.py 102 5 95% 49, 133, 163, 167-168
wisdomagent/domain/errors.py 39 0 100%
wisdomagent/domain/evaluation.py 140 6 96% 45, 47, 51, 70, 72, 264
wisdomagent/domain/events.py 97 27 72% 65, 67, 69, 73, 78, 93, 97-112, 155, 161, 167, 173, 179
wisdomagent/domain/fusion.py 76 5 93% 78, 82, 101, 105, 119
wisdomagent/domain/grounding.py 152 15 90% 68-73, 76, 139, 147, 163, 190, 249, 251, 253, 274
wisdomagent/domain/lexical.py 109 15 86% 66, 68, 96-97, 105, 125-127, 134, 181, 214-218
wisdomagent/domain/model.py 308 20 94% 50, 56, 203-211, 215, 242, 247, 249, 276, 282, 361, 446, 448
wisdomagent/domain/monitors.py 178 8 96% 99-102, 123, 125, 127, 332
wisdomagent/domain/partition.py 59 0 100%
wisdomagent/domain/planner.py 97 3 97% 142, 171, 217
wisdomagent/domain/rerank.py 105 10 90% 64, 84, 104, 107, 116, 128, 142, 188, 223, 253
wisdomagent/domain/tokenize.py 82 14 83% 79, 95, 105, 127-136, 143
wisdomagent/domain/vector.py 174 53 70% 69, 71, 122, 124, 126, 138, 155-159, 164, 174-177, 200, 212-231, 241, 254-255, 257-261, 263, 282-285, 288, 299-306, 317-319
wisdomagent/observability/__init__.py 4 0 100%
wisdomagent/observability/logging.py 48 0 100%
wisdomagent/observability/metrics.py 115 0 100%
wisdomagent/resilience/__init__.py 2 0 100%
wisdomagent/resilience/resilience.py 203 7 97% 67, 86, 88, 136, 162, 242, 313
wisdomagent/worker.py 76 13 83% 111-124, 128, 132
----------------------------------------------------------------------
TOTAL 4712 422 91%
893 passed, 1 warning in 7.23sTests are named as behavioural statements (test_every_claim_cites_a_retrieved_document, test_refuses_an_unanswerable_question, test_every_recorded_step_is_also_counted) and grouped in classes by the guarantee they defend. Roughly a third of them are failure-path tests, because the failure paths are where a grounded-answer system earns its keep.
Project layout
wisdomagent/
domain/ 15 modules — pure logic, zero I/O imports
app/ ports, CQRS services, AnswerSaga, mediator
adapters/ memory, synthetic, postgres, messaging, redisx, agent/, mcp/
api/ FastAPI app, GraphQL schema, mappers, request context
observability/ structured logging, metrics registry, tracing
resilience/ timeout, retry, circuit breaker, bulkhead, rate limiter
config/ settings parsing and validation
container.py composition root
main.py API entrypoint
worker.py monitor scheduler entrypoint
tests/ 24 files, 893 tests (incl. an architecture test
asserting the domain imports no infrastructure)
benchmarks/ complexity-gated performance suite
db/migrations/ partitioned + sharded schema
monitoring/ prometheus, 27 alerts, alertmanager, otel, 2 grafana dashboards
postman/ importable collection, schema-validatedTrade-offs and known gaps
Stated plainly, because a README that only lists strengths is not useful.
The default embedder is hashed, not learned. It is deterministic, needs no model download and makes the eval harness reproducible — but it captures far less semantic similarity than a real sentence encoder. The
SearchIndexport exists so a learned embedder is a drop-in replacement.The offline drafter is extractive. It quotes rather than composes, so answers read as a list of verbatim spans. This is why
answer.modelis returned: consumers can tell which path produced the text.The worker does not consume Kafka. It publishes digest events and runs the monitor scheduler, but the consumer-side projection is not implemented. The publisher and topic configuration are real; nothing reads them back.
Grounding uses lexical span containment, not entailment. It cannot detect a claim that is semantically implied but not lexically overlapping, and it can over-credit a paraphrase that reuses vocabulary. A cross-encoder NLI model would be strictly better and strictly slower; the
GroundingCheckeris a single class behind a stable interface.The GraphQL surface has no authentication. Tenancy is a header, with an explicit note in
Context.tenant_orthat a real multi-customer deployment must authenticate it and reject mismatched overrides. This is a deliberate scope boundary, not an oversight.domain/events.pysits at 72% coverage, the lowest in the tree — the event constructors are mostly data plumbing and several variants are only exercised indirectly.The complexity gate measures a single machine. Exponents are robust to hardware; the absolute numbers in this README are not, and CI deliberately gates on the former only.
License
MIT — see LICENSE.
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
- Flicense-qualityCmaintenanceAn MCP server that exposes document retrieval as tools (semantic search and source listing) for any LLM, using a vector index built from DocPilot's ingestion pipeline.Last updated
- AlicenseBqualityAmaintenanceA lightweight MCP server that enables querying a project's corpus (docs, decisions, issues, skills) with cited answers and typed refusals via stdio JSON-RPC 2.0.Last updated9MIT
- Alicense-qualityBmaintenanceMCP server that grounds AI answers in a local, maintained knowledge base and optionally fills gaps from the web, fully local with SQLite.Last updatedAGPL 3.0
- Flicense-qualityCmaintenanceAn MCP server that exposes grounded, source-attributed question-answering over a collection of PDF documents.Last updated
Related MCP Connectors
Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.
Local-first RAG engine with MCP server for AI agent integration.
MCP server exposing the Backtest360 engine API as tools for AI agents.
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/ABHIJEET-MUNESHWAR/WisdomAgent'
If you have feedback or need assistance with the MCP directory API, please join our Discord server