Skip to main content
Glama
QuantQJ
by QuantQJ

filings-search

Hybrid (BM25 + kNN) agentic retrieval over SEC 10-K filings, on OpenSearch, with a Claude tool-use agent that plans its own searches and answers with per-claim citations and a hard numeric-grounding check. FastAPI surface + TypeScript MCP tool server. Built as the OpenSearch successor to my Qdrant pipeline; the cite-or-abstain grounding is ported from my 12-domain provenance wrapper.

EDGAR ──► connector ──► section splitter ──► chunker ──► embeddings ──► OpenSearch (BM25 + HNSW kNN)
(SEC API)  (ticker→CIK,   (Item 1/1A/1C/7/8…)  (450 tok,    (nomic-embed-text            │
            10-K list,     order-constrained,    60 overlap)  local via Ollama, or        │
            iXBRL strip)   x-ref filtered)                     OpenAI)                     ▼
                                                                          hybrid search (RRF) + filters
                                                                                       │
                                                       ┌───────────────────────────────┼─────────────────────┐
                                                       ▼                               ▼                     ▼
                                              FastAPI /search /ask           TS MCP server            eval harness
                                              /chunk /companies         (search_filings, ask_filings)  (P@k/MRR, LLM-judge)
                                                       ▲
                                              Claude agent (tool loop):
                                              resolve_company → search_filings (item/ticker/FY filters, hybrid|bm25|knn)
                                              → expand_chunk → answer with [c:chunk_id] cites → numeric grounding check

What's in it

Layer

File

Notes

Connector

filings_search/edgar.py

SEC ticker map + browse-edgar fallback (handles successor-shell CIKs), data.sec.gov submissions, primary-doc fetch, iXBRL/HTML strip, disk cache

Parsing

filings_search/parse.py

10-K Item splitter: title-verified headings, cross-reference filter, canonical-order + longest-span selection (defeats ToC rows)

Chunking

filings_search/chunk.py

paragraph-respecting, token-bounded (450/60 overlap), deterministic chunk ids `sha1(accession

Embeddings

filings_search/embed.py

nomic-embed-text (768-d) on local Ollama by default; OpenAI text-embedding-3-small fallback

Index

filings_search/index.py

OpenSearch 2.19 mapping: english analyzer BM25 field + knn_vector (lucene HNSW, cosine) + keyword metadata (ticker, cik, item, fiscal_year, accession…)

Retrieval

filings_search/search.py

bm25, knn, hybrid (client-side reciprocal-rank fusion), metadata filters, neighbor expansion

Agent

filings_search/agent.py

Claude (claude-opus-5) tool loop; tools: list_indexed_companies, resolve_company, search_filings, expand_chunk; answer must cite [c:id]; every figure in the answer must appear in a cited chunk or the response is flagged grounded_numbers=false

API

filings_search/api.py

FastAPI: GET /search, POST /ask, GET /chunk/{id}?expand=, GET /companies, GET /health

MCP

mcp/src/server.ts

TypeScript stdio MCP server exposing list_companies, search_filings, get_chunk, ask_filings

Eval

eval/

run_retrieval_eval.py (hit@1/hit@5/MRR@10 by mode, labeled queries), run_grounding_eval.py (numeric grounding + LLM-as-judge citation support)

Run it

docker compose up -d                       # OpenSearch 2.19 (knn + neural plugins), :9200
ollama pull nomic-embed-text               # local embeddings
python3.12 -m venv .venv && ./.venv/bin/pip install -r requirements.txt
./.venv/bin/python ingest.py --recreate AAPL MSFT NVDA JPM XOM WMT TSLA JNJ   # ~2 min, 8 filings, ~2.1k chunks
./run_api.sh                               # FastAPI on :8801
./.venv/bin/python -m filings_search.agent "What does NVIDIA disclose about export controls to China?"
./.venv/bin/python eval/run_retrieval_eval.py
./.venv/bin/python eval/run_grounding_eval.py

MCP (Claude Desktop / Claude Code / Cursor):

{"mcpServers": {"filings-search": {"command": "node", "args": ["/ABS/PATH/filings-search/mcp/dist/server.js"],
                                    "env": {"FILINGS_API_URL": "http://127.0.0.1:8801"}}}}

Config via env: FS_OPENSEARCH_URL, FS_INDEX, FS_EMBED_BACKEND=ollama|openai, FS_AGENT_MODEL, FS_JUDGE_MODEL, SEC_USER_AGENT. ANTHROPIC_API_KEY (or ~/.env) for the agent/judge.

Results (2026-08-18, 8 filings / 2,101 chunks, 28 labeled queries)

Retrieval — see eval/retrieval_results.json:

setting

mode

hit@1

hit@5

MRR@10

avg ms

unfiltered

bm25

0.571

0.857

0.686

5.7

unfiltered

knn

0.679

0.857

0.759

29.7

unfiltered

hybrid

0.679

0.857

0.759

37.9

ticker_filtered

bm25

0.679

0.929

0.772

3.8

ticker_filtered

knn

0.679

0.893

0.779

26.6

ticker_filtered

hybrid

0.714

0.929

0.812

34.9

Agent grounding — 10 analyst questions, claude-opus-5 agent + claude-opus-5 judge (see eval/grounding_results.json):

metric

value

numeric-grounded rate (every figure appears in a cited chunk)

10/10 = 1.00

LLM-judge verdict: grounded / partially grounded / ungrounded

9 / 1 / 0

mean fraction of claims supported by a cited chunk

0.968

avg citations per answer

9.7

avg tool calls per answer (agent-chosen searches/expansions)

7.6

avg latency

41 s (13 s simple → 59 s multi-Item)

tokens for the 10-question run

563k in / 23k out

The one partially_grounded (Walmart tariffs, 0.87) was the agent summarizing a mitigation that the cited chunk states more narrowly — the judge caught it; that is what the judge is for.

Design notes / honest limitations

  • Agentic ≠ fixed RAG. The model chooses company, Item, mode, and how many rounds; a fixed top-k pipeline gets no second chance. The tool descriptions carry the "when to use" guidance (item map, bm25-for-figures, widen-if-empty).

  • Grounding is strict on purpose. A derived rounding ("$99,779M" → "~$99.8B") is flagged as ungrounded; analysts want the figure as filed. Loosen with a tolerance if you disagree.

  • 10-K structure quirks are real, not parser bugs: JPM and XOM are "wrapper" 10-Ks whose MD&A/financials sit in a back-of-book Financial Section (labeled under the last Item); NVIDIA files statements under Item 15. Eval labels for those are ticker-only. A follow-up is F-page detection (Consolidated Statements of … headings) to relabel as Item 8.

  • RRF is client-side — transparent and easy to reason about; OpenSearch's hybrid query

    • normalization pipeline is the in-cluster alternative. No cross-encoder re-ranker yet.

  • Single filing per company in this run; --filings N pulls prior years (fiscal_year filter already in the mapping and tools).

  • No auth, no rate limits, single-node OpenSearch — this is a working vertical, not a deployment.

-
license - not tested
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

  • SEC EDGAR for AI agents: company filings, financials and insider trades. No API keys.

  • SEC EDGAR filings for AI agents: company lookup, filings, financials, insider trades. No keys.

  • Provide AI assistants with real-time access to official SEC EDGAR filings and financial data. Enab…

View all MCP Connectors

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/QuantQJ/filings-search'

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