OmniFetcher
One typed shape for every data source.
Jira, Postgres, PDFs, S3, Slack, Kafka, GitHub, Confluence — 30 sources, one contract. The code that reads one reads them all. Ships an MCP server, so your LLM agents can fetch from any of them through a single tool.
pip install omni-fetcher
The problem, in one screen
Pulling from four sources today means four SDKs, four auth models, four response shapes, and four ways to fail:
# GitHub — PyGithub
issue = Github(token).get_repo("psf/requests").get_issue(42)
text = issue.body # comments? separate call, separate shape
# Jira — atlassian-python-api
data = Jira(url, username=u, password=t).issue("PROJ-1")
text = data["fields"]["description"] # dict-diving, and it raises on 404
# A PDF — pypdf
text = "\n".join(p.extract_text() for p in PdfReader("report.pdf").pages)
# Postgres — asyncpg
rows = await (await asyncpg.connect(dsn)).fetch("SELECT id, email FROM users LIMIT 10")
text = ... # rows aren't text. and read-only? that's on you.
Now write the error handling. Four times. Now add a fifth source.
With OmniFetcher, it's one loop:
from omni_fetcher.v1 import AtomKind, OmniFetcher, Success, builtin_registry
omni = OmniFetcher(builtin_registry())
for uri in [
"https://github.com/psf/requests/issues/42",
"jira://issue/PROJ-1",
"report.pdf",
"postgres://db.internal/app?query=SELECT%20id,email%20FROM%20users%20LIMIT%2010",
]:
result = await omni.fetch(uri, auth=creds_for(uri)) # your credential lookup
if isinstance(result, Success): # -> one typed Result, every source
text = "\n".join(a.content for a in result.tree.find_atoms(AtomKind.TEXT))
print(uri, "→", len(text), "chars")
Same call. Same result type. Same three moves every time: check the state,
walk the tree, read the atoms. No if source == "jira". Ever.
Who it's for
- Building RAG or agent context — you need many heterogeneous sources in one shape, with provenance and honest failures, not thirty bespoke parsers.
- Internal tools and glue services — one dependency instead of a dozen SDKs, and one error model instead of a dozen exception hierarchies.
- Anyone wiring an LLM to real data — the MCP server does it with no glue code at all.
For agents: one MCP server, every source
pip install "omni-fetcher[mcp]"
omni-fetcher-mcp is a stdio MCP server that
hands a model three tools:
| Tool | What it does |
|---|---|
fetch(uri) |
The canonical result as JSON, for any bounded source |
sample(uri, max_items) |
A bounded window over an unbounded stream (Kafka, a log tail, CDC) — peek without hanging |
list_sources() |
What this install can reach, and which are streams |
Credentials are host-configured (OMNI_FETCHER_GITHUB_TOKEN,
OMNI_FETCHER_POSTGRES_PASSWORD, …) and injected per call — a token never
enters the model's context. Point Claude Desktop, Claude Code, or any MCP
client at it and the model pulls a Jira ticket, a database row, and a PDF into
context through one interface.
Two agent skills ship too: one teaches an agent the contract, one assembles context from a set of sources.
How it compares
Honest positioning — these tools are good at what they do; they solve a differently-shaped problem:
| Great at | Where OmniFetcher differs | |
|---|---|---|
| LangChain / LlamaIndex loaders | Enormous loader breadth, deep framework integration | Each loader returns its own document/metadata shape and raises on failure. OmniFetcher emits one typed contract across every source, returns failures as values, and is framework-free — no chain, no index, no agent runtime to adopt |
| unstructured.io | Best-in-class document parsing into elements | Parsing files is one slice. OmniFetcher spans APIs, SaaS, databases, and live streams under the same shape (and will happily hand a PDF to a parser like theirs) |
| Airbyte / Singer | Production ELT — scheduled syncs into a warehouse | That's deployed infrastructure and batch pipelines. OmniFetcher is an in-process library for on-demand, request-time fetches: an agent's context, a RAG build, an internal endpoint |
| Just calling each SDK | Total control, zero abstraction | Perfect for one or two sources. The cost is N auth models, N response shapes, and N failure modes — to learn, and to maintain |
Why it's built this way
The connectors are interchangeable plumbing. The typed shape they emit is the point — and these are the bets behind it, all tested:
- Errors are values, not exceptions. A missing resource, a bad credential, a
parse failure comes back as a typed
Error(kind)you branch on — not atry/exceptyou forgot to write. Partial data comes back asPartialwith typed gaps naming exactly what's missing, so you never silently lose the half that worked. - Stateless, multi-tenant by construction. Connectors hold no credentials and no data between calls; you pass auth per call. One shared orchestrator serves every tenant concurrently — proven by a test that drives it from 16 threads and 64 interleaved coroutines and asserts zero cross-tenant leakage.
- Read-only means read-only — enforced by the engine, not a regex. The SQL
connectors run every query inside a
READ ONLYtransaction (Postgres/MySQL) or amode=roopen (SQLite). A write is refused by the database, not by us trying to parse your SQL. Verified against real Postgres, MySQL, and MariaDB. - Zoom is semantic, not token-chopping. Ask for text at paragraph or sentence depth and you get the source's own structure, split losslessly — never an arbitrary character window. Ask for a depth a source can't provide and you get an honest gap, not a silent no-op.
The full rationale — including what OmniFetcher deliberately refuses to be — is in PHILOSOPHY.md.
The contract
Every fetch returns a Result. That's the whole surface you branch on:
Result
├── Success ── tree: CompositionNode
├── Partial ── tree + gaps: list[Gap] # what came back, plus typed holes
└── Error ──── kind: ErrorKind + message # returned as a value, never raised
CompositionNode # exactly two fields — no surprises
├── metadata: Metadata # id, kind, tags, created, updated, author, source_url
│ # + source_extra["<source>"] for the source-only fields
└── children: [ ... ] # a MIXED list, in document order:
# CompositionNode | Text | Image | Audio | Video | Table
Content and metadata are separate on purpose: fields every source has (id,
author, timestamps, source_url) are typed on Metadata; anything only one
source has is namespaced under source_extra["jira"], so it can't collide with
your generic code and can't leak into it.
30 sources — and counting
Documents & files — PDF, DOCX, PPTX, CSV, audio, local files, any URL
Local paths and file://, .pdf, .docx/.pptx (the office extra), .csv,
audio, plus any https:// page or JSON/GraphQL/RSS endpoint.
SaaS & collaboration — Jira, Confluence, Slack, Notion, Linear, GitHub, Google Drive, SharePoint, YouTube
Each with its own URI scheme (jira://issue/KEY, slack://channel/ID, …) and
per-call auth. The Atlassian ones need the jira / confluence extra.
Databases — Postgres, MySQL/MariaDB, SQLite, Postgres CDC
postgres://, mysql:///mariadb://, sqlite:// run a read query and return a
Table atom, read-only enforced by the engine. postgres-cdc:// streams
row-level INSERT/UPDATE/DELETE via logical replication. SQLite needs no extra.
Search & storage — Elasticsearch, S3
es://… drives the scroll API and returns one document per hit;
s3://bucket/key with AwsAuth.
Streams — Kafka, Redis Streams, log tails, WebSocket, SSE
Unbounded sources consumed with stream() (or the MCP sample tool). Each item
carries its resume position, and stream_with_restart reconnects across a
dropped connection without loss.
Don't trust a list in a README — ask the installed package what it can actually reach (only sources whose extras are installed show up):
omni-fetcher v1 sources # a table of every routable source + bounded/stream flag
omni-fetcher v1 sources --json # the same, as JSON
60 seconds
import asyncio
from omni_fetcher.v1 import AtomKind, Success
from omni_fetcher.v1.connectors.local_file import LocalFileFetcher
async def main() -> None:
result = await LocalFileFetcher().fetch("README.md")
if isinstance(result, Success):
for atom in result.tree.find_atoms(AtomKind.TEXT):
print(atom.content[:100])
asyncio.run(main())
Optional extras gate a source or capability: office, jira, confluence,
elasticsearch, kafka, websockets, postgres, mysql, gdrive, web,
mcp. Everything else works on the core install.
A few more moves
Handle all three states — expected failures are values:
result = await connector.fetch(uri)
if isinstance(result, Success):
... # result.tree
elif isinstance(result, Partial):
... # result.tree + result.gaps (don't throw this away)
elif isinstance(result, Error):
... # result.kind, result.message
Auth per call — nothing is stored, which is what makes it multi-tenant-safe:
await JiraConnector().fetch("jira://issue/PROJ-1",
auth=BasicAuth(username="dev@acme.io", password="api-token"))
await SlackConnector().fetch("slack://channel/C0123456789",
auth=BearerAuth(token="xoxb-..."))
Consume a stream — resumable across drops:
async for item in omni.stream("tail:///var/log/app.log?from=end"):
if isinstance(item, Success):
print(item.tree.find_atoms(AtomKind.TEXT)[0].content)
Or skip Python — the CLI takes credentials as env-var names, so no secret
touches argv:
omni-fetcher v1 fetch "sqlite:///app.db?query=SELECT%20*%20FROM%20users" --json
omni-fetcher v1 fetch "jira://issue/PROJ-1" \
--auth-type basic --username-env JIRA_USER --password-env JIRA_TOKEN
omni-fetcher v1 stream "kafka://localhost:9092/events?offset=earliest" --json --max-items 100
Write a connector — subclass BaseFetcher, override stream(), and
fetch() comes free. Any built-in's exact URI/auth/options live in its module
docstring (python -c "from omni_fetcher.v1.connectors import mysql; print(mysql.__doc__)"),
which never drifts from the code.
Status
v1.10, MIT, ~1,300 tests, Python 3.10–3.12. The v1 contract is stable and additive — new connectors slot in without changing the shape you code against. Issues and connector contributions welcome; if a source you need is missing, the custom-connector path above is about twenty lines.
Development
git clone https://github.com/Jainil-Gosalia/omni_fetcher
cd omni_fetcher
pip install -e ".[dev,office,web,gdrive,confluence,slack,jira,kafka,mcp,mysql]"
pytest --ignore=tests/integration # unit + contract suites
ruff check . && ruff format --check . && mypy omni_fetcher/
CI runs the same gates on Python 3.10–3.12. Releases are tag-driven: a GitHub
Release vX.Y.Z builds, gates, and publishes to PyPI via trusted publishing.
The pre-1.0 API still ships unchanged; docs/migration-v1.md
maps it onto the contract above.
License
MIT © 2024–2026 OmniFetcher Contributors
Metadata
Release files for omni-fetcher 1.17.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| omni_fetcher-1.17.0.tar.gz | 640.0 kB | With provenance | Details for omni_fetcher-1.17.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| omni_fetcher-1.17.0-py3-none-any.whl | Python 3 | none | any | Details for omni_fetcher-1.17.0-py3-none-any.whl |
Total release size: 1.1 MB
Release files / omni_fetcher-1.17.0.tar.gz
| Download URL | omni_fetcher-1.17.0.tar.gz |
|---|---|
| Size | 640.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
37bf593ff9a2b58fafc12f6d7f4ed97eeb0f92b8f842524ccb5e31afedc4a1c4
|
|
BLAKE2b-256 checksum How to use checksums |
7c21079dd9b4f5df4b150e0e1c4eba9a5e3955bdec6a5a7ab11980b6442b9a90
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.
Transparency logRelease files / omni_fetcher-1.17.0-py3-none-any.whl
| Download URL | omni_fetcher-1.17.0-py3-none-any.whl |
|---|---|
| Size | 420.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f91685cdc5fd76fa52ca3621ef0d9d43c186a8ded0bae9fd81fcaedb9d7ea258
|
|
BLAKE2b-256 checksum How to use checksums |
e3db9570b7babf88993e1f7126d8e8ef11bc4a4d6c16d8e8f8385f6ad7e1c451
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.
Transparency logSecurity for omni-fetcher 1.17.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for omni_fetcher-1.17.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Jul 30, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.