Skip to main content
Glama

numenews

coverage python license

MCP server and one-shot CLI that reads the news numerologically.

numenews fetches news from several public APIs, extracts numbers, dates and names, computes numerology (digit reduction, master numbers 11/22/33, gematria), finds patterns with hybrid vector search in Qdrant, and builds a daily forecast — while keeping every number activation as long-term memory.

It is a pet project that takes its subject seriously and itself not very: the numerology layer is pure, deterministic and property-tested, the retrieval is a real hybrid search over a real vector store, and the default demo path needs no API keys at all. v0.1.0 is out and both interfaces work.

What it does

  • Nine MCP tools over stdio, so Claude Desktop, Cursor or any MCP host can fetch news, extract numbers, find patterns, build a forecast and query the memory.

  • Six one-shot commands — today, forecast, history, search, patterns, mcp — each printing one JSON document on stdout and nothing else, so they pipe straight into jq.

  • Five news sources (GDELT, NewsAPI, GNews, Mediastack, Currents) behind one NewsSource Protocol, with an aggregator that degrades gracefully when a feed is down or rate-limited, and an RFC 9111 cache so a repeated run does not re-hit the network.

  • Local embeddings only — two bge models through fastembed. No embedding API, ever.

  • Six Qdrant collections with semantic and hybrid (reciprocal-rank-fusion) search, and payload indexes that make filtered queries cheap.

  • Four pydantic-ai agents — extract numbers, find patterns, build the forecast, summarise old news — every one returning a validated Pydantic model rather than free-form JSON.

  • A pipeline that composes them: ingest → analyze → forecast, with a sliding window over the last week and a digest for everything older.

  • Long-term memory: every number activation is logged, folded into a per-day frequency, and fed back into the forecast over a thirty-day window.

  • A ragas evaluation of the retrieval path, isolated in its own environment so LangChain never reaches the runtime.

Related MCP server: Google News MCP Agent

Quick start

uv sync --all-extras        # install the environment (Python 3.14+, uv)
make dev                    # start Qdrant (Docker Compose) and wait until healthy
cp .env.example .env        # optional: the default demo path needs no API keys
make lint && make test      # ruff + mypy --strict, pytest with coverage

Qdrant's dashboard is then at http://localhost:6333/dashboard.

Over the CLI

today needs Qdrant and an LLM endpoint; history, search and patterns need Qdrant only. The full command surface is in docs/USER_FLOW.md.

uv run numenews today | jq .dominant_number   # ingest the window and read the day
uv run numenews forecast --date tomorrow      # a stored/derived reading, no fetch
uv run numenews history --number 11 --days 30 # when 11 was active
uv run numenews search -q "число 7 и деньги"  # hybrid search over stored news
uv run numenews patterns --min-strength 0.7   # the strong patterns, strongest first
uv run numenews --help

Over MCP

The server starts without Qdrant or an API key and builds each dependency on the first tool call that needs it, so it can be attached to a host right away. Cursor's project config ships in .cursor/mcp.json; Claude Desktop takes the same command in its own claude_desktop_config.json. The nine tools, their examples and the failure semantics are in docs/MCP_TOOLS.md.

make mcp                                            # serve stdio (Ctrl-D to stop)
uv run pytest tests/integration/test_mcp_stdio.py   # the same handshake, as a test

What the output looks like

The pipeline builds this, the CLI serializes it, and stdout is always JSON:

{
  "date": "2026-09-21",
  "dominant_number": 11,
  "master_active": true,
  "patterns": [
    {
      "id": "8bb3a3e4-53cd-5333-92ab-5309c63d3b78",
      "type": "resonance",
      "numbers": [11, 22],
      "news_ids": ["b371bc46-7b4b-5b38-92db-cdf94a550f33"],
      "strength": 0.87,
      "interpretation": "Числа 11 и 22 резонируют в новостях о технологиях",
      "discovered_at": "2026-09-21T12:00:00Z"
    }
  ],
  "forecast": "День благоприятен для начинаний, связанных с коммуникацией",
  "advice": "Избегайте конфликтов — число 11 усиливает эмоции",
  "warnings": ["Возможны повторяющиеся события из прошлого"]
}

How OpenSpec is used here

Specs are the source of truth for behaviour, and they live in the repository next to the code.

  • openspec/specs/ holds what the project ships today, one capability per directory. Two exist so far: mcp-surface (the nine tools — inputs, return shapes, error reporting) and cli-surface (the six commands — JSON-only stdout, the --date grammar, exit codes, idempotency). The internals are not specified yet; they get a capability the first time a change really touches them.

  • openspec/changes/ holds work in flight: a proposal, the spec deltas it would make, a design when the approach needs deciding, and a task list.

  • The loop is propose → review → implement → archive. openspec list shows what is active, openspec validate "<name>" --strict checks a change, and archiving merges its spec deltas into openspec/specs/.

  • New work starts as a change, not as an edit. An idea that is not a change yet lives in a GitHub issue.

docs/MCP_TOOLS.md and docs/USER_FLOW.md remain the narrative companions to the two specs: the specs say what the behaviour must be, those documents show how to use it.

Project layout

src/numenews/
├── config.py       # pydantic-settings
├── logging.py      # structlog + contextvars correlation
├── models/         # Pydantic v2 domain models
├── numerology/     # pure logic: reduction, master numbers, gematria (no I/O)
├── news/           # GDELT, NewsAPI, GNews, Mediastack, Currents behind one Protocol
├── embeddings/     # fastembed wrapper
├── vector/         # Qdrant client, collections, hybrid search
├── agents/         # pydantic-ai: extract, pattern, forecast, summarize
├── pipeline/       # the RAG chain, the sliding window, step timings
├── mcp/            # MCP server and 9 tools
└── cli/            # one-shot Typer commands

openspec/
├── specs/          # behaviour that ships, one capability per directory
└── changes/        # proposals in flight

Layer

Choice

Package manager

uv, single package, src/ layout

Types

pydantic v2 + mypy --strict

Vector store

Qdrant (Docker Compose; :memory: in tests)

Embeddings

fastembed (bge-small-en-v1.5 384d, bge-base-en-v1.5 768d) — local, no API keys

LLM orchestration

pydantic-ai with structured output

MCP server

Python mcp SDK v2 (MCPServer), nine tools over stdio

HTTP

httpx + hishel (RFC 9111 cache) + tenacity

Config / logs

pydantic-settings / structlog (stderr, JSON in prod)

CLI

Typer + Rich, JSON-only stdout

Tests

pytest + pytest-asyncio + respx + hypothesis + ragas (eval)

Commands

make help          # list every target
make install       # uv sync --all-extras
make lint          # ruff check + format check + mypy
make test          # unit + integration (with coverage)
make test-eval     # ragas evaluation in .venv-eval (needs an LLM endpoint; see docs/EVAL.md)
make eval-env      # build .venv-eval (ragas; ADR 0013)
make dev           # docker compose up -d --wait
make dev-down      # stop Qdrant, keep the volume
make clean         # stop Qdrant, delete volumes and caches
make run           # sample one-shot CLI run (needs Qdrant and an LLM endpoint)
make mcp           # start the MCP server (stdio)

make test runs the unit and integration suites with coverage and then checks the per-layer floors (numerology ≥ 95 %, pipeline/agents ≥ 80 %, vector/news ≥ 70 %); the levels, the doubles and the dated coverage snapshot are in docs/TESTING.md.

Documentation

Contributing

It is a solo pet project, so there is no ceremony — but there is a workflow, and CONTRIBUTING.md spells it out. The short version: work starts as an OpenSpec change rather than a direct edit, one tasks.md item is one atomic task, and make lint && make test has to be green. Issues and ideas are welcome; there is no CI, on purpose, which is why the commands are the gate.

License

MIT

Available Tools

9 tools
build_forecastA

Return the numerological reading for a calendar day, saving it for later calls.

A day that was already read answers from Qdrant without running a model. Otherwise the reading is assembled from the last seven days of stored news (their dominant number, whether a master number is active, and the patterns among them) plus the activations of exactly those numbers over the pipeline's thirty-day memory window, and then written back. Needs Qdrant and an LLM endpoint.

Inputs: day (required). Returns: One day's reading. ``dominant_number`` is the reduced value the day is read und…

check_master_numbersA

Report the master numbers (11, 22, 33) among a list of numbers.

master_numbers lists the distinct master numbers found, ascending, and count how many occurrences there were: [11, 11] gives has_master=True, master_numbers=(11,) and count=2. An empty list is a valid input and reports no master number. Pure logic: no configuration, no service.

Inputs: numbers (required). Returns: Outcome of :func:`numenews.numerology.check_master_numbers`. ``master_numbers``…

compute_numerologyA

Reduce a text to its numerological value.

Returns the raw gematria sum, the digit-reduced value (11, 22 and 33 are master numbers and are not reduced further), whether the value is master, and the rendered calculation steps. Pure logic: no Qdrant, no model, no configuration.

Inputs: text (required). Returns: The reading of one text. ``gematria`` is the raw letter sum, ``value`` the redu…

extract_numbersA

Read the numbers, dates and symbols out of a text with the extraction agent.

The model's reading is combined with the deterministic regex pass, so a mention the model formats differently ("eleven" beside "11") is still found, and a model that cannot answer at all degrades to the regex reading instead of failing. sources says which strategies contributed. Needs an LLM endpoint (OPENAI_API_KEY or OPENAI_BASE_URL).

Inputs: text (required). Returns: Numbers, symbols and provenance produced from one text. ``sources`` names the e…

fetch_newsA

Fetch the news of a topic in an inclusive date range from every configured source.

The five feeds run in parallel and the result is merged, de-duplicated and filtered to the range. A source that fails is skipped with a warning, so a broken feed thins the result instead of failing the call; if no source is configured at all (no GDELT, which needs no key) the call is a tool error.

Inputs: topic (required), date_range (required). Returns: result

find_patternsA

Find the connections among the stored news items with these ids, save them and return them.

The ids come from an earlier fetch_news/ingest run or from query_qdrant. An id that is not in the store is skipped rather than refused, and an empty list answers [] without asking the model — "nothing connects" and "nothing to look at" stay tellable apart. Each returned pattern carries the discovered_at timestamp save_pattern stamped. Needs Qdrant and an LLM endpoint.

Inputs: news_ids (required). Returns: result

get_historyA

Return when a number was activated in the news, newest first, inside the last days days.

One entry per (news item, number) pair the ingest stored: the day the article was published, the item's id and the snippet the number was read in. The window ends today and includes it (days=1 is today). The original design sketch wrote the signature as get_history(number); the read is a window, so days is a parameter whose default of 30 matches the memory window. Needs Qdrant only.

Inputs: number (required), days. Returns: result

query_qdrantA

Search the stored news or patterns by meaning and return the matching entities.

collection="news" runs the hybrid (multi-stage) search over stored articles and honours the optional payload filters — publication window, source, reduced value or master number. collection="patterns" finds saved pattern interpretations and takes no filters: passing any is an error rather than a silently ignored argument. Needs Qdrant only; nothing is embedded remotely and no model is called.

The returned items are the typed entities themselves (NewsItem or Pattern), best match first; the scores stay in Qdrant because the callers work with the entities.

Inputs: query (required), collection (required), limit, filters. Returns: What ``query_qdrant`` found: the collection it searched and the entities it ret…

save_patternA

Store one pattern and return it as written, with discovered_at filled in.

The original design sketch called the return type SavedPattern; the saved model is a Pattern (the one numenews.vector.save_pattern returns), so the tool returns that rather than a second type saying the same thing. Re-saving the same pattern id overwrites the point instead of adding a copy, and a timestamp already present is never rewritten. Needs Qdrant only.

Inputs: pattern (required). Returns: A connection between news items with a confidence. ``strength`` is bounded to `…

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4/5.0

    Scored across 9 tools

    Disambiguation5/5

    Each tool has a clearly distinct purpose: build_forecast, save_pattern, get_history, fetch_news, extract_numbers, compute_numerology, find_patterns, check_master_numbers, and query_qdrant target different domain actions. The descriptions distinguish overlapping-sounding tools (e.g., compute_numerology vs extract_numbers vs check_master_numbers) by scope and input/output.

    Naming Consistency5/5

    Names consistently follow a verb_noun pattern (build_forecast, save_pattern, get_history, fetch_news, extract_numbers, compute_numerology, find_patterns, check_master_numbers, query_qdrant). The only noun-only name is query_qdrant, which still fits the pattern loosely as a verb-mechanism combination.

    Tool Count5/5

    Nine tools is well-scoped for a numerological news analysis pipeline. Each tool covers a distinct stage (ingest, extract, compute, find, save, query, forecast) with no obvious redundancy or missing pieces at the count level.

    Completeness4/5

    The surface covers fetch/extract/analyze/save/query/forecast, which is a complete lifecycle for the domain. Minor gaps include no direct delete or update for stored patterns/news, and no batch operations, but agents can work around these via query and re-save.

    Maintenance

    ActivitySlowing
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      D
      maintenance
      Enables fact-checking of news headlines using Google Gemini AI and web search verification, and retrieves trending news topics from RSS feeds with comprehensive multi-source analysis.
      1
      -
    • F
      license
      B
      quality
      D
      maintenance
      Fetches news articles from Google News, vectorizes them locally using ChromaDB, and enables semantic search through natural language queries.
      2
      -
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables LLM agents to discover news sources, list articles, parse and extract article text, search news, and perform NLP analysis such as keyword extraction and summarization.
      MIT
    • F
      license
      Not graded
      quality
      B
      maintenance
      Enables searching and fetching news articles with configurable date ranges, returning structured, sourced evidence for daily mineral-rights briefings.
      -