diff --git a/changelog.d/20260914_aryabov_cli.rst b/changelog.d/20260914_aryabov_cli.rst
new file mode 100644
index 00000000..8bbb86e8
--- /dev/null
+++ b/changelog.d/20260914_aryabov_cli.rst
@@ -0,0 +1,4 @@
+Added
+-----
+
+* Add a command-line tool that outputs normalized feed data as JSON.
diff --git a/feedparser/cli.py b/feedparser/cli.py
new file mode 100644
index 00000000..3f486828
--- /dev/null
+++ b/feedparser/cli.py
@@ -0,0 +1,27 @@
+import argparse
+import json
+import sys
+
+from .api import parse
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("source", help="feed URL, filename, or - for stdin")
+ args = parser.parse_args()
+
+ source = sys.stdin.buffer if args.source == "-" else args.source
+ feed = parse(source)
+ if feed.bozo:
+ raise feed.bozo_exception
+
+ json.dump(
+ {"feed": feed.feed, "entries": feed.entries},
+ sys.stdout,
+ ensure_ascii=False,
+ indent=2,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pyproject.toml b/pyproject.toml
index fae60798..8aea144b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -19,6 +19,9 @@ dependencies = [
"requests>=2.20.0",
]
+[project.scripts]
+feedparser = "feedparser.cli:main"
+
[project.urls]
Source = "https://github.com/kurtmckee/feedparser"
Documentation = "https://feedparser.readthedocs.io/en/latest/"
diff --git a/tests/test_cli.py b/tests/test_cli.py
new file mode 100644
index 00000000..dfb77d12
--- /dev/null
+++ b/tests/test_cli.py
@@ -0,0 +1,55 @@
+import json
+import subprocess
+import sys
+
+import pytest
+
+FEED = """\
+
+
+
+ Test feed
+ https://example.com/
+ Test feed
+ -
+ Café
+ https://example.com/cafe
+
+
+
+"""
+
+
+@pytest.mark.parametrize("from_stdin", [False, True])
+def test_cli_outputs_entries(tmp_path, from_stdin):
+ path = tmp_path / "feed.xml"
+ path.write_text(FEED)
+ source = "-" if from_stdin else str(path)
+
+ result = subprocess.run(
+ [sys.executable, "-m", "feedparser.cli", source],
+ input=FEED if from_stdin else None,
+ capture_output=True,
+ text=True,
+ )
+
+ assert result.returncode == 0
+ output = json.loads(result.stdout)
+ assert output["feed"]["title"] == "Test feed"
+ assert output["entries"][0]["title"] == "Café"
+ assert "Café" in result.stdout
+ assert '\n "feed": {' in result.stdout
+ assert result.stderr == ""
+
+
+def test_cli_fails_on_malformed_feed():
+ result = subprocess.run(
+ [sys.executable, "-m", "feedparser.cli", "-"],
+ input="",
+ capture_output=True,
+ text=True,
+ )
+
+ assert result.returncode != 0
+ assert result.stdout == ""
+ assert result.stderr