anki-addons-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. anki_addons_mcp-0.1.0/.gitignore +9 -0
  2. anki_addons_mcp-0.1.0/LICENSE +21 -0
  3. anki_addons_mcp-0.1.0/PKG-INFO +114 -0
  4. anki_addons_mcp-0.1.0/README.md +98 -0
  5. anki_addons_mcp-0.1.0/pyproject.toml +83 -0
  6. anki_addons_mcp-0.1.0/src/anki_addons_mcp/__init__.py +1 -0
  7. anki_addons_mcp-0.1.0/src/anki_addons_mcp/__main__.py +56 -0
  8. anki_addons_mcp-0.1.0/src/anki_addons_mcp/adapters/__init__.py +0 -0
  9. anki_addons_mcp-0.1.0/src/anki_addons_mcp/adapters/hf_parquet/__init__.py +0 -0
  10. anki_addons_mcp-0.1.0/src/anki_addons_mcp/adapters/hf_parquet/mapping.py +106 -0
  11. anki_addons_mcp-0.1.0/src/anki_addons_mcp/adapters/hf_parquet/repository.py +352 -0
  12. anki_addons_mcp-0.1.0/src/anki_addons_mcp/adapters/hf_parquet/source.py +187 -0
  13. anki_addons_mcp-0.1.0/src/anki_addons_mcp/adapters/hf_parquet/sql.py +132 -0
  14. anki_addons_mcp-0.1.0/src/anki_addons_mcp/adapters/memory/__init__.py +0 -0
  15. anki_addons_mcp-0.1.0/src/anki_addons_mcp/adapters/memory/repository.py +217 -0
  16. anki_addons_mcp-0.1.0/src/anki_addons_mcp/application/__init__.py +0 -0
  17. anki_addons_mcp-0.1.0/src/anki_addons_mcp/application/catalog.py +116 -0
  18. anki_addons_mcp-0.1.0/src/anki_addons_mcp/application/compatibility.py +58 -0
  19. anki_addons_mcp-0.1.0/src/anki_addons_mcp/application/history.py +100 -0
  20. anki_addons_mcp-0.1.0/src/anki_addons_mcp/application/maintenance.py +108 -0
  21. anki_addons_mcp-0.1.0/src/anki_addons_mcp/application/services.py +49 -0
  22. anki_addons_mcp-0.1.0/src/anki_addons_mcp/application/similarity.py +76 -0
  23. anki_addons_mcp-0.1.0/src/anki_addons_mcp/application/snapshots.py +38 -0
  24. anki_addons_mcp-0.1.0/src/anki_addons_mcp/application/stats.py +38 -0
  25. anki_addons_mcp-0.1.0/src/anki_addons_mcp/bootstrap.py +69 -0
  26. anki_addons_mcp-0.1.0/src/anki_addons_mcp/config.py +32 -0
  27. anki_addons_mcp-0.1.0/src/anki_addons_mcp/domain/__init__.py +0 -0
  28. anki_addons_mcp-0.1.0/src/anki_addons_mcp/domain/compatibility.py +88 -0
  29. anki_addons_mcp-0.1.0/src/anki_addons_mcp/domain/errors.py +25 -0
  30. anki_addons_mcp-0.1.0/src/anki_addons_mcp/domain/models.py +221 -0
  31. anki_addons_mcp-0.1.0/src/anki_addons_mcp/domain/ports.py +63 -0
  32. anki_addons_mcp-0.1.0/src/anki_addons_mcp/domain/query.py +52 -0
  33. anki_addons_mcp-0.1.0/src/anki_addons_mcp/interface/__init__.py +0 -0
  34. anki_addons_mcp-0.1.0/src/anki_addons_mcp/interface/mcp/__init__.py +0 -0
  35. anki_addons_mcp-0.1.0/src/anki_addons_mcp/interface/mcp/errors.py +31 -0
  36. anki_addons_mcp-0.1.0/src/anki_addons_mcp/interface/mcp/prompts.py +91 -0
  37. anki_addons_mcp-0.1.0/src/anki_addons_mcp/interface/mcp/resources.py +84 -0
  38. anki_addons_mcp-0.1.0/src/anki_addons_mcp/interface/mcp/server.py +42 -0
  39. anki_addons_mcp-0.1.0/src/anki_addons_mcp/interface/mcp/tools.py +174 -0
  40. anki_addons_mcp-0.1.0/tests/__init__.py +0 -0
  41. anki_addons_mcp-0.1.0/tests/adapters/__init__.py +0 -0
  42. anki_addons_mcp-0.1.0/tests/adapters/test_sql_sandbox.py +63 -0
  43. anki_addons_mcp-0.1.0/tests/application/__init__.py +0 -0
  44. anki_addons_mcp-0.1.0/tests/application/test_compatibility_rules.py +59 -0
  45. anki_addons_mcp-0.1.0/tests/application/test_services.py +163 -0
  46. anki_addons_mcp-0.1.0/tests/conftest.py +35 -0
  47. anki_addons_mcp-0.1.0/tests/contract/__init__.py +0 -0
  48. anki_addons_mcp-0.1.0/tests/contract/test_repository_contract.py +201 -0
  49. anki_addons_mcp-0.1.0/tests/fixtures/__init__.py +0 -0
  50. anki_addons_mcp-0.1.0/tests/fixtures/dataset.py +180 -0
  51. anki_addons_mcp-0.1.0/tests/interface/__init__.py +0 -0
  52. anki_addons_mcp-0.1.0/tests/interface/test_mcp_server.py +164 -0
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ build/
9
+ .idea
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aleksey Yablokov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.5
2
+ Name: anki-addons-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server providing access to the Anki Addons dataset (HuggingFace Ya-Alex/anki-addons).
5
+ Project-URL: Homepage, https://github.com/Aleks-Ya/anki-addons-mcp
6
+ Project-URL: Repository, https://github.com/Aleks-Ya/anki-addons-mcp
7
+ Project-URL: Issues, https://github.com/Aleks-Ya/anki-addons-mcp/issues
8
+ Project-URL: Dataset, https://huggingface.co/datasets/Ya-Alex/anki-addons
9
+ Author-email: Aleksey Yablokov <alex_ya@mailbox.org>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: addons,anki,dataset,huggingface,mcp
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Education
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: >=3.13
21
+ Requires-Dist: duckdb
22
+ Requires-Dist: huggingface-hub
23
+ Requires-Dist: mcp
24
+ Requires-Dist: pydantic-settings
25
+ Requires-Dist: pydantic>=2
26
+ Requires-Dist: uvicorn
27
+ Description-Content-Type: text/markdown
28
+
29
+ # Anki Addons MCP
30
+
31
+ An [MCP](https://modelcontextprotocol.io) server that gives LLM clients read-only access to the
32
+ [Anki Addons dataset](https://huggingface.co/datasets/Ya-Alex/anki-addons): every addon in the AnkiWeb
33
+ catalog, enriched with GitHub, Anki Forum and AI-summary data, published as monthly snapshots.
34
+
35
+ It answers questions such as "which addon does X?", "will it work on Anki 25.02?", "is it maintained?",
36
+ and "what changed since last month?".
37
+
38
+ ## Links
39
+ - [Anki Addons dataset](https://huggingface.co/datasets/Ya-Alex/anki-addons) on HuggingFace
40
+ - [anki-addons-dataset](https://github.com/Aleks-Ya/anki-addons-dataset), the pipeline that builds the dataset
41
+ - [Developer Guide](https://github.com/Aleks-Ya/anki-addons-mcp/blob/main/README-DEV.md): running from sources, debugging, tests, architecture, deployment
42
+
43
+ ## Use it locally (STDIO)
44
+
45
+ Requires [uv](https://docs.astral.sh/uv/). `uvx` runs the [PyPI package](https://pypi.org/project/anki-addons-mcp/)
46
+ in an isolated, cached environment; to run a working copy instead, see
47
+ [Run from sources](https://github.com/Aleks-Ya/anki-addons-mcp/blob/main/README-DEV.md#run-from-sources).
48
+
49
+ Register the server in Claude Code:
50
+
51
+ ```bash
52
+ claude mcp add anki-addons -- uvx anki-addons-mcp
53
+ ```
54
+
55
+ Other MCP clients use the same command (`uvx anki-addons-mcp`) in their server configuration, e.g.:
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "anki-addons": { "command": "uvx", "args": ["anki-addons-mcp"] }
61
+ }
62
+ }
63
+ ```
64
+
65
+ The first start downloads the latest snapshot (~15 MB) into the HuggingFace cache. Older snapshots are
66
+ downloaded on demand by history tools, so the first history question can take a minute.
67
+
68
+ ## Use it remotely (Streamable HTTP)
69
+
70
+ Connect to a hosted instance (e.g. a HuggingFace Space) by URL:
71
+
72
+ ```bash
73
+ claude mcp add --transport http anki-addons https://<user>-anki-addons-mcp.hf.space/mcp
74
+ ```
75
+
76
+ To host your own instance, see [Run over HTTP](https://github.com/Aleks-Ya/anki-addons-mcp/blob/main/README-DEV.md#run-over-http) and
77
+ [Deploy to a HuggingFace Space](https://github.com/Aleks-Ya/anki-addons-mcp/blob/main/README-DEV.md#deploy-to-a-huggingface-space).
78
+
79
+ ## Capabilities
80
+
81
+ **Tools** (all read-only, every data tool takes an optional `snapshot`, `YYYY-MM-DD` or `latest`):
82
+ `search_addons`, `filter_addons`, `get_addon`, `get_addon_readme`, `get_addon_description`,
83
+ `check_compatibility`, `assess_maintenance`, `find_similar_addons`, `list_addons_by_author`,
84
+ `get_addon_history`, `diff_snapshots`, `get_statistics`, `top_addons`, `list_snapshots`, and
85
+ `query_sql` (one sandboxed DuckDB `SELECT` over an `addons` table; only offered by SQL-capable backends).
86
+
87
+ **Resources**: `anki-addons://dataset/readme`, `anki-addons://dataset/schema`, `anki-addons://snapshots`,
88
+ `anki-addons://snapshots/{snapshot}/metadata`, `anki-addons://snapshots/{snapshot}/aggregation`,
89
+ `anki-addons://addons/{addon_id}` (and `/readme`, `/description`, `/history`),
90
+ `anki-addons://authors/{github_user}`.
91
+
92
+ **Prompts**: `find_addon_for_task`, `compare_addons`, `addon_health_report`, `upgrade_readiness`,
93
+ `monthly_ecosystem_digest`, `before_you_build`, `author_portfolio`.
94
+
95
+ AI summaries in the dataset are LLM-generated; the server labels them as such.
96
+
97
+ ## Configuration
98
+
99
+ CLI flags override environment variables (prefix `ANKI_MCP_`, also read from `.env`).
100
+
101
+ | Variable | Default | Meaning |
102
+ |---|---|---|
103
+ | `ANKI_MCP_TRANSPORT` | `stdio` | `stdio` or `http` |
104
+ | `ANKI_MCP_HOST` / `ANKI_MCP_PORT` / `ANKI_MCP_PATH` | `127.0.0.1` / `8000` / `/mcp` | HTTP binding |
105
+ | `ANKI_MCP_ALLOWED_HOSTS` | `[]` | JSON list of Host headers to allow (enables DNS-rebinding protection) |
106
+ | `ANKI_MCP_BACKEND` | `hf_parquet` | `hf_parquet` (HuggingFace Hub) or `local_parquet` (a local copy) |
107
+ | `ANKI_MCP_HF_REPO_ID` / `ANKI_MCP_HF_REVISION` | `Ya-Alex/anki-addons` / `main` | Dataset to serve |
108
+ | `ANKI_MCP_DATA_DIR` | | Dataset directory for `local_parquet` (`latest/`, `history/<date>/`) |
109
+ | `ANKI_MCP_CACHE_DIR` | HF cache | Download cache |
110
+ | `ANKI_MCP_REFRESH_INTERVAL_SECONDS` | `3600` | Check for a new dataset revision; `0` disables |
111
+ | `ANKI_MCP_SQL_ENABLED` / `_SQL_MAX_ROWS` / `_SQL_TIMEOUT_SECONDS` | `true` / `200` / `10` | `query_sql` limits |
112
+ | `ANKI_MCP_MAINTENANCE_ACTIVE_DAYS` / `_STALE_DAYS` | `180` / `730` | Maintenance label thresholds |
113
+ | `ANKI_MCP_LOG_LEVEL` | `INFO` | Logs go to stderr |
114
+ | `HF_TOKEN` | | Optional HuggingFace token for higher download rate limits (a Space secret) |
@@ -0,0 +1,98 @@
1
+ ---
2
+ title: Anki Addons MCP
3
+ emoji: 🗂️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: mit
10
+ short_description: MCP server for the Anki Addons dataset
11
+ ---
12
+
13
+ # Anki Addons MCP
14
+
15
+ An [MCP](https://modelcontextprotocol.io) server that gives LLM clients read-only access to the
16
+ [Anki Addons dataset](https://huggingface.co/datasets/Ya-Alex/anki-addons): every addon in the AnkiWeb
17
+ catalog, enriched with GitHub, Anki Forum and AI-summary data, published as monthly snapshots.
18
+
19
+ It answers questions such as "which addon does X?", "will it work on Anki 25.02?", "is it maintained?",
20
+ and "what changed since last month?".
21
+
22
+ ## Links
23
+ - [Anki Addons dataset](https://huggingface.co/datasets/Ya-Alex/anki-addons) on HuggingFace
24
+ - [anki-addons-dataset](https://github.com/Aleks-Ya/anki-addons-dataset), the pipeline that builds the dataset
25
+ - [Developer Guide](README-DEV.md): running from sources, debugging, tests, architecture, deployment
26
+
27
+ ## Use it locally (STDIO)
28
+
29
+ Requires [uv](https://docs.astral.sh/uv/). `uvx` runs the [PyPI package](https://pypi.org/project/anki-addons-mcp/)
30
+ in an isolated, cached environment; to run a working copy instead, see
31
+ [Run from sources](README-DEV.md#run-from-sources).
32
+
33
+ Register the server in Claude Code:
34
+
35
+ ```bash
36
+ claude mcp add anki-addons -- uvx anki-addons-mcp
37
+ ```
38
+
39
+ Other MCP clients use the same command (`uvx anki-addons-mcp`) in their server configuration, e.g.:
40
+
41
+ ```json
42
+ {
43
+ "mcpServers": {
44
+ "anki-addons": { "command": "uvx", "args": ["anki-addons-mcp"] }
45
+ }
46
+ }
47
+ ```
48
+
49
+ The first start downloads the latest snapshot (~15 MB) into the HuggingFace cache. Older snapshots are
50
+ downloaded on demand by history tools, so the first history question can take a minute.
51
+
52
+ ## Use it remotely (Streamable HTTP)
53
+
54
+ Connect to a hosted instance (e.g. a HuggingFace Space) by URL:
55
+
56
+ ```bash
57
+ claude mcp add --transport http anki-addons https://<user>-anki-addons-mcp.hf.space/mcp
58
+ ```
59
+
60
+ To host your own instance, see [Run over HTTP](README-DEV.md#run-over-http) and
61
+ [Deploy to a HuggingFace Space](README-DEV.md#deploy-to-a-huggingface-space).
62
+
63
+ ## Capabilities
64
+
65
+ **Tools** (all read-only, every data tool takes an optional `snapshot`, `YYYY-MM-DD` or `latest`):
66
+ `search_addons`, `filter_addons`, `get_addon`, `get_addon_readme`, `get_addon_description`,
67
+ `check_compatibility`, `assess_maintenance`, `find_similar_addons`, `list_addons_by_author`,
68
+ `get_addon_history`, `diff_snapshots`, `get_statistics`, `top_addons`, `list_snapshots`, and
69
+ `query_sql` (one sandboxed DuckDB `SELECT` over an `addons` table; only offered by SQL-capable backends).
70
+
71
+ **Resources**: `anki-addons://dataset/readme`, `anki-addons://dataset/schema`, `anki-addons://snapshots`,
72
+ `anki-addons://snapshots/{snapshot}/metadata`, `anki-addons://snapshots/{snapshot}/aggregation`,
73
+ `anki-addons://addons/{addon_id}` (and `/readme`, `/description`, `/history`),
74
+ `anki-addons://authors/{github_user}`.
75
+
76
+ **Prompts**: `find_addon_for_task`, `compare_addons`, `addon_health_report`, `upgrade_readiness`,
77
+ `monthly_ecosystem_digest`, `before_you_build`, `author_portfolio`.
78
+
79
+ AI summaries in the dataset are LLM-generated; the server labels them as such.
80
+
81
+ ## Configuration
82
+
83
+ CLI flags override environment variables (prefix `ANKI_MCP_`, also read from `.env`).
84
+
85
+ | Variable | Default | Meaning |
86
+ |---|---|---|
87
+ | `ANKI_MCP_TRANSPORT` | `stdio` | `stdio` or `http` |
88
+ | `ANKI_MCP_HOST` / `ANKI_MCP_PORT` / `ANKI_MCP_PATH` | `127.0.0.1` / `8000` / `/mcp` | HTTP binding |
89
+ | `ANKI_MCP_ALLOWED_HOSTS` | `[]` | JSON list of Host headers to allow (enables DNS-rebinding protection) |
90
+ | `ANKI_MCP_BACKEND` | `hf_parquet` | `hf_parquet` (HuggingFace Hub) or `local_parquet` (a local copy) |
91
+ | `ANKI_MCP_HF_REPO_ID` / `ANKI_MCP_HF_REVISION` | `Ya-Alex/anki-addons` / `main` | Dataset to serve |
92
+ | `ANKI_MCP_DATA_DIR` | | Dataset directory for `local_parquet` (`latest/`, `history/<date>/`) |
93
+ | `ANKI_MCP_CACHE_DIR` | HF cache | Download cache |
94
+ | `ANKI_MCP_REFRESH_INTERVAL_SECONDS` | `3600` | Check for a new dataset revision; `0` disables |
95
+ | `ANKI_MCP_SQL_ENABLED` / `_SQL_MAX_ROWS` / `_SQL_TIMEOUT_SECONDS` | `true` / `200` / `10` | `query_sql` limits |
96
+ | `ANKI_MCP_MAINTENANCE_ACTIVE_DAYS` / `_STALE_DAYS` | `180` / `730` | Maintenance label thresholds |
97
+ | `ANKI_MCP_LOG_LEVEL` | `INFO` | Logs go to stderr |
98
+ | `HF_TOKEN` | | Optional HuggingFace token for higher download rate limits (a Space secret) |
@@ -0,0 +1,83 @@
1
+ [build-system]
2
+ requires = ["hatchling", "hatch-fancy-pypi-readme"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "anki-addons-mcp"
7
+ dynamic = ["version", "readme"]
8
+ description = "MCP server providing access to the Anki Addons dataset (HuggingFace Ya-Alex/anki-addons)."
9
+ requires-python = ">=3.13"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Aleksey Yablokov", email = "alex_ya@mailbox.org" }]
13
+ keywords = ["anki", "addons", "mcp", "dataset", "huggingface"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Education",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ ]
23
+ dependencies = [
24
+ "mcp",
25
+ "pydantic>=2",
26
+ "pydantic-settings",
27
+ "duckdb",
28
+ "huggingface-hub",
29
+ "uvicorn",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/Aleks-Ya/anki-addons-mcp"
34
+ Repository = "https://github.com/Aleks-Ya/anki-addons-mcp"
35
+ Issues = "https://github.com/Aleks-Ya/anki-addons-mcp/issues"
36
+ Dataset = "https://huggingface.co/datasets/Ya-Alex/anki-addons"
37
+
38
+ [project.scripts]
39
+ anki-addons-mcp = "anki_addons_mcp.__main__:main"
40
+
41
+ [tool.hatch.version]
42
+ path = "src/anki_addons_mcp/__init__.py"
43
+
44
+ [tool.hatch.metadata.hooks.fancy-pypi-readme]
45
+ content-type = "text/markdown"
46
+
47
+ [[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
48
+ path = "README.md"
49
+ start-at = "# Anki Addons MCP"
50
+
51
+ [[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
52
+ pattern = '\]\((?!https?://)([^)]+)\)'
53
+ replacement = '](https://github.com/Aleks-Ya/anki-addons-mcp/blob/main/\1)'
54
+
55
+ [tool.hatch.build.targets.sdist]
56
+ include = ["src", "tests", "README.md", "LICENSE"]
57
+
58
+ [tool.hatch.build.targets.wheel]
59
+ packages = ["src/anki_addons_mcp"]
60
+
61
+ [dependency-groups]
62
+ dev = [
63
+ "pytest",
64
+ "anyio",
65
+ "ruff",
66
+ "mypy",
67
+ "pyarrow",
68
+ "bump-my-version",
69
+ ]
70
+
71
+ [tool.pytest.ini_options]
72
+ testpaths = ["tests"]
73
+
74
+ [tool.ruff]
75
+ line-length = 120
76
+
77
+ [tool.mypy]
78
+ python_version = "3.13"
79
+ files = ["src"]
80
+
81
+ [[tool.mypy.overrides]]
82
+ module = ["anki_addons_mcp.domain.*", "anki_addons_mcp.application.*"]
83
+ strict = true
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,56 @@
1
+ import argparse
2
+ import logging
3
+ import sys
4
+ from typing import Any
5
+
6
+ from anki_addons_mcp import __version__
7
+ from anki_addons_mcp.bootstrap import create_app
8
+ from anki_addons_mcp.config import Settings
9
+
10
+
11
+ def _parse_args(argv: list[str] | None) -> dict[str, Any]:
12
+ parser = argparse.ArgumentParser(prog="anki-addons-mcp", description="MCP server for the Anki Addons dataset")
13
+ parser.add_argument("--version", action="version", version=__version__)
14
+ parser.add_argument("--transport", choices=["stdio", "http"])
15
+ parser.add_argument("--host")
16
+ parser.add_argument("--port", type=int)
17
+ parser.add_argument("--path")
18
+ parser.add_argument("--backend", choices=["hf_parquet", "local_parquet"])
19
+ parser.add_argument("--data-dir", dest="data_dir")
20
+ parser.add_argument("--cache-dir", dest="cache_dir")
21
+ parser.add_argument("--hf-revision", dest="hf_revision")
22
+ parser.add_argument("--refresh-interval", dest="refresh_interval_seconds", type=int)
23
+ parser.add_argument("--no-sql", dest="sql_enabled", action="store_const", const=False)
24
+ parser.add_argument("--log-level", dest="log_level", choices=["DEBUG", "INFO", "WARNING", "ERROR"])
25
+ return {k: v for k, v in vars(parser.parse_args(argv)).items() if v is not None}
26
+
27
+
28
+ def _run_http(mcp: Any, settings: Settings) -> None:
29
+ import uvicorn
30
+ from mcp.server.transport_security import TransportSecuritySettings
31
+
32
+ security = None
33
+ if settings.allowed_hosts:
34
+ security = TransportSecuritySettings(enable_dns_rebinding_protection=True,
35
+ allowed_hosts=settings.allowed_hosts)
36
+ app = mcp.streamable_http_app(streamable_http_path=settings.path, json_response=True, stateless_http=True,
37
+ host=settings.host, transport_security=security)
38
+ uvicorn.run(app, host=settings.host, port=settings.port, log_level=settings.log_level.lower())
39
+
40
+
41
+ def main(argv: list[str] | None = None) -> None:
42
+ settings = Settings(**_parse_args(argv))
43
+ logging.basicConfig(stream=sys.stderr, level=settings.log_level,
44
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s")
45
+ if settings.log_level != "DEBUG":
46
+ for noisy in ("httpx", "httpx2", "huggingface_hub"):
47
+ logging.getLogger(noisy).setLevel(logging.WARNING)
48
+ mcp, _ = create_app(settings)
49
+ if settings.transport == "http":
50
+ _run_http(mcp, settings)
51
+ else:
52
+ mcp.run("stdio")
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()
@@ -0,0 +1,106 @@
1
+ import json
2
+ from typing import Any
3
+
4
+ from anki_addons_mcp.domain.models import (
5
+ AddonCard,
6
+ AddonDetails,
7
+ AiInfo,
8
+ AnkiWebInfo,
9
+ Branch,
10
+ ForumInfo,
11
+ GithubInfo,
12
+ GithubLink,
13
+ LanguageBytes,
14
+ Manifest,
15
+ )
16
+
17
+ CARD_COLUMNS = [
18
+ "id", "title", "url", "rating", "likes", "dislikes", "update_date", "anki_version", "ai_summary", "ai_model",
19
+ "github_user", "github_repo", "github_stars", "github_last_commit", "github_archived", "github_license",
20
+ "forum_posts_count",
21
+ ]
22
+
23
+
24
+ def _list(value: Any) -> list[Any]:
25
+ return [v for v in value if v is not None] if isinstance(value, list) else []
26
+
27
+
28
+ def _structs(value: Any) -> list[dict[str, Any]]:
29
+ return [v for v in _list(value) if isinstance(v, dict)]
30
+
31
+
32
+ def _manifest(value: Any) -> Manifest | None:
33
+ if not value:
34
+ return None
35
+ raw = json.loads(value) if isinstance(value, str) else value
36
+ if not isinstance(raw, dict):
37
+ return None
38
+ raw["conflicts"] = _list(raw.get("conflicts"))
39
+ return Manifest.model_validate(raw)
40
+
41
+
42
+ def row_to_card(row: dict[str, Any], score: float | None = None) -> AddonCard:
43
+ return AddonCard(
44
+ id=row["id"], title=row["title"] or "", url=row["url"] or "", rating=row["rating"] or 0,
45
+ likes=row["likes"] or 0, dislikes=row["dislikes"] or 0, update_date=row["update_date"] or None,
46
+ anki_version=row["anki_version"] or None, ai_summary=row["ai_summary"], ai_model=row["ai_model"],
47
+ github_user=row["github_user"], github_repo=row["github_repo"], github_stars=row["github_stars"],
48
+ github_last_commit=row["github_last_commit"], github_archived=row["github_archived"],
49
+ license=row["github_license"], forum_posts_count=row["forum_posts_count"], score=score,
50
+ )
51
+
52
+
53
+ def row_to_details(row: dict[str, Any]) -> AddonDetails:
54
+ anki_web = AnkiWebInfo(
55
+ title=row["title"] or "",
56
+ addon_page_url=row["url"] or "",
57
+ contact_author_url=row["contact_author_url"],
58
+ rating=row["rating"] or 0,
59
+ update_date=row["update_date"] or None,
60
+ anki_version=row["anki_version"] or None,
61
+ branches=[Branch.model_validate(b) for b in _structs(row["branches"])],
62
+ links=_list(row["links"]),
63
+ likes=row["likes"] or 0,
64
+ dislikes=row["dislikes"] or 0,
65
+ ai_declaration_markers=_list(row["ai_declaration_markers"]),
66
+ description_language=row["description_language"],
67
+ description_language_confidence=row["description_language_confidence"],
68
+ )
69
+ github = None
70
+ if row["has_github"]:
71
+ github = GithubInfo(
72
+ user=row["github_user"],
73
+ repo=row["github_repo"],
74
+ languages=_list(row["github_languages"]),
75
+ stars=row["github_stars"] or 0,
76
+ last_commit=row["github_last_commit"],
77
+ links=[GithubLink.model_validate(link) for link in _structs(row["github_links"]) if link.get("url")],
78
+ action_count=row["github_action_count"],
79
+ tests_count=row["github_tests_count"],
80
+ license=row["github_license"],
81
+ forks=row["github_forks"],
82
+ open_issues=row["github_open_issues"],
83
+ size_kb=row["github_size_kb"],
84
+ topics=_list(row["github_topics"]),
85
+ repo_description=row["github_repo_description"],
86
+ homepage=row["github_homepage"],
87
+ archived=row["github_archived"],
88
+ pushed_at=row["github_pushed_at"],
89
+ created_at=row["github_created_at"],
90
+ primary_language=row["github_primary_language"],
91
+ language_bytes=[LanguageBytes.model_validate(lb) for lb in _structs(row["github_language_bytes"])],
92
+ manifest=_manifest(row["github_manifest"]),
93
+ dependencies=_list(row["github_dependencies"]),
94
+ ai_tooling_markers=_list(row["github_ai_tooling_markers"]),
95
+ )
96
+ forum = None
97
+ if row["has_forum"] or row["forum_topic_id"] is not None:
98
+ forum = ForumInfo(
99
+ anki_forum_url=row["forum_url"],
100
+ topic_slug=row["forum_topic_slug"],
101
+ topic_id=row["forum_topic_id"],
102
+ last_posted_at=row["forum_last_posted_at"],
103
+ posts_count=row["forum_posts_count"],
104
+ )
105
+ ai = AiInfo(summary=row["ai_summary"], model=row["ai_model"]) if row["ai_summary"] or row["ai_model"] else None
106
+ return AddonDetails(id=row["id"], anki_web=anki_web, github=github, forum=forum, ai=ai)