iflow-mcp_ujisati_anki-mcp 0.1.0__py3-none-any.whl

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.
anki_mcp/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+ import asyncio
2
+
3
+ from fastmcp import FastMCP
4
+
5
+ from .card_service import card_mcp
6
+ from .deck_service import deck_mcp
7
+ from .media_service import media_mcp
8
+ from .model_service import model_mcp
9
+ from .note_service import note_mcp
10
+
11
+
12
+ anki_mcp = FastMCP(
13
+ name="AnkiConnectMCP",
14
+ instructions="""
15
+ This MCP provides a programmatic interface to Anki flashcard functionality through the AnkiConnect API.
16
+ It allows AI assistants to interact with Anki decks, cards, notes, models, and media
17
+ without needing to understand the underlying API details. All interactions are through tools.
18
+ """,
19
+ )
20
+
21
+
22
+ async def setup(run_server: bool = True):
23
+ await anki_mcp.import_server(deck_mcp)
24
+ await anki_mcp.import_server(note_mcp)
25
+ await anki_mcp.import_server(card_mcp)
26
+ await anki_mcp.import_server(model_mcp)
27
+ await anki_mcp.import_server(media_mcp)
28
+ if run_server:
29
+ await anki_mcp.run_async()
30
+
31
+
32
+ def main():
33
+ asyncio.run(setup(run_server=True))
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()
@@ -0,0 +1,116 @@
1
+ from typing import Annotated, Any, Dict, List, Optional
2
+
3
+ from fastmcp import FastMCP
4
+ from pydantic import Field
5
+
6
+ from .common import anki_call
7
+
8
+ card_mcp = FastMCP(name="AnkiCardService")
9
+
10
+
11
+ @card_mcp.tool(
12
+ name="findCards",
13
+ description="Returns an array of card IDs for a given Anki search query.",
14
+ )
15
+ async def find_cards_tool(
16
+ query: Annotated[
17
+ str, Field(description="Anki search query (e.g., 'deck:current is:new').")
18
+ ],
19
+ ) -> List[int]:
20
+ return await anki_call("findCards", query=query)
21
+
22
+
23
+ @card_mcp.tool(
24
+ name="cardsInfo",
25
+ description="Returns a list of objects containing information for each card ID provided.",
26
+ )
27
+ async def get_cards_info_tool(
28
+ cards: Annotated[List[int], Field(description="A list of card IDs.")],
29
+ ) -> List[Dict[str, Any]]:
30
+ return await anki_call("cardsInfo", cards=cards)
31
+
32
+
33
+ @card_mcp.tool(
34
+ name="cardsToNotes",
35
+ description="Returns an unordered array of note IDs for the given card IDs.",
36
+ )
37
+ async def convert_cards_to_notes_tool(
38
+ cards: Annotated[List[int], Field(description="A list of card IDs.")],
39
+ ) -> List[int]:
40
+ return await anki_call("cardsToNotes", cards=cards)
41
+
42
+
43
+ @card_mcp.tool(
44
+ name="areSuspended",
45
+ description="Returns an array indicating whether each given card is suspended. Each item is boolean or null if the card doesn't exist.",
46
+ )
47
+ async def check_cards_suspended_tool(
48
+ cards: Annotated[List[int], Field(description="A list of card IDs.")],
49
+ ) -> List[Optional[bool]]:
50
+ return await anki_call("areSuspended", cards=cards)
51
+
52
+
53
+ @card_mcp.tool(
54
+ name="cardsModTime",
55
+ description="Returns modification time for each card ID provided. Result is a list of objects with 'cardId' and 'modTime' (timestamp).",
56
+ )
57
+ async def get_cards_modification_time_tool(
58
+ cards: Annotated[List[int], Field(description="A list of card IDs.")],
59
+ ) -> List[Dict[str, Any]]:
60
+ return await anki_call("cardsModTime", cards=cards)
61
+
62
+
63
+ @card_mcp.tool(
64
+ name="suspended",
65
+ description="Checks if a single card is suspended by its ID. Returns true if suspended, false otherwise.",
66
+ )
67
+ async def check_card_suspended_tool(
68
+ card: Annotated[int, Field(description="The ID of the card.")],
69
+ ) -> bool:
70
+ return await anki_call("suspended", card=card)
71
+
72
+
73
+ @card_mcp.tool(
74
+ name="suspend", description="Suspends the specified cards. Returns true on success."
75
+ )
76
+ async def suspend_cards_tool(
77
+ cards: Annotated[List[int], Field(description="A list of card IDs to suspend.")],
78
+ ) -> bool:
79
+ return await anki_call("suspend", cards=cards)
80
+
81
+
82
+ @card_mcp.tool(
83
+ name="unsuspend",
84
+ description="Unsuspends the specified cards. Returns true on success.",
85
+ )
86
+ async def unsuspend_cards_tool(
87
+ cards: Annotated[List[int], Field(description="A list of card IDs to unsuspend.")],
88
+ ) -> bool:
89
+ return await anki_call("unsuspend", cards=cards)
90
+
91
+
92
+ @card_mcp.tool(
93
+ name="setSpecificValueOfCard",
94
+ description="Sets specific values of a single card. Use with caution. Returns list of booleans indicating success for each key.",
95
+ )
96
+ async def set_specific_card_value_tool(
97
+ card: Annotated[int, Field(description="The ID of the card to modify.")],
98
+ keys: Annotated[
99
+ List[str],
100
+ Field(
101
+ description="List of card property keys to change (e.g., 'flags', 'odue')."
102
+ ),
103
+ ],
104
+ newValues: Annotated[
105
+ List[Any],
106
+ Field(description="List of new values corresponding to the keys."),
107
+ ],
108
+ warning_check: Annotated[
109
+ Optional[bool],
110
+ Field(description="Set to True for potentially risky operations."),
111
+ ] = None,
112
+ ) -> List[bool]:
113
+ params: Dict[str, Any] = {"card": card, "keys": keys, "newValues": newValues}
114
+ if warning_check is not None:
115
+ params["warning_check"] = warning_check
116
+ return await anki_call("setSpecificValueOfCard", **params)
anki_mcp/common.py ADDED
@@ -0,0 +1,23 @@
1
+ from typing import Any
2
+
3
+ import httpx
4
+
5
+ ANKICONNECT_URL = "http://127.0.0.1:8765"
6
+
7
+
8
+ async def anki_call(action: str, **params: Any) -> Any:
9
+ async with httpx.AsyncClient() as client:
10
+ payload = {"action": action, "version": 6, "params": params}
11
+ result = await client.post(ANKICONNECT_URL, json=payload)
12
+ result.raise_for_status()
13
+ result_json = result.json()
14
+ error = result_json.get("error")
15
+ if error:
16
+ raise Exception(f"AnkiConnect error for action '{action}': {error}")
17
+ response = result_json.get("result")
18
+
19
+
20
+
21
+ if "result" in result_json:
22
+ return response
23
+ return result_json
@@ -0,0 +1,91 @@
1
+ from typing import Annotated, Any, Dict, List
2
+
3
+ from fastmcp import FastMCP
4
+ from pydantic import Field
5
+
6
+ from .common import anki_call
7
+
8
+ deck_mcp = FastMCP(name="AnkiDeckService")
9
+
10
+
11
+ @deck_mcp.tool(
12
+ name="deckNamesAndIds",
13
+ description="Gets the complete list of deck names and their respective IDs. Returns a dictionary mapping deck names to their IDs.",
14
+ )
15
+ async def list_deck_names_and_ids_tool() -> Dict[str, int]:
16
+ return await anki_call("deckNamesAndIds")
17
+
18
+
19
+ @deck_mcp.tool(
20
+ name="getDeckConfig",
21
+ description="Gets the configuration group object for the given deck name. Returns the deck configuration object.",
22
+ )
23
+ async def get_deck_config_tool(
24
+ deck: Annotated[str, Field(description="The name of the deck (e.g., 'Default').")],
25
+ ) -> Dict[str, Any]:
26
+ return await anki_call("getDeckConfig", deck=deck)
27
+
28
+
29
+ @deck_mcp.tool(
30
+ name="deckNames",
31
+ description="Gets the complete list of deck names for the current user. Returns a list of deck names.",
32
+ )
33
+ async def list_deck_names_tool() -> List[str]:
34
+ return await anki_call("deckNames")
35
+
36
+
37
+ @deck_mcp.tool(
38
+ name="createDeck",
39
+ description="Creates a new empty deck. Will not overwrite an existing deck with the same name. Returns the ID of the created deck.",
40
+ )
41
+ async def create_deck_tool(
42
+ deck: Annotated[
43
+ str,
44
+ Field(description="The name of the deck to create (e.g., 'Japanese::Tokyo')."),
45
+ ],
46
+ ) -> int:
47
+ return await anki_call("createDeck", deck=deck)
48
+
49
+
50
+ @deck_mcp.tool(
51
+ name="deleteDecks",
52
+ description="Deletes decks with the given names. The 'cardsToo' argument must be specified and set to true.",
53
+ )
54
+ async def delete_decks_tool(
55
+ decks: Annotated[List[str], Field(description="A list of deck names to delete.")],
56
+ cardsToo: Annotated[
57
+ bool,
58
+ Field(
59
+ description="Must be true to confirm deletion of cards within the decks."
60
+ ),
61
+ ],
62
+ ) -> None:
63
+ if not cardsToo:
64
+ raise ValueError("cardsToo must be true to delete decks.")
65
+ return await anki_call("deleteDecks", decks=decks, cardsToo=cardsToo)
66
+
67
+
68
+ @deck_mcp.tool(
69
+ name="changeDeck",
70
+ description="Moves cards with the given IDs to a different deck, creating the deck if it doesn't exist yet.",
71
+ )
72
+ async def change_deck_tool(
73
+ cards: Annotated[List[int], Field(description="A list of card IDs to move.")],
74
+ deck: Annotated[str, Field(description="The target deck name.")],
75
+ ) -> None:
76
+ return await anki_call("changeDeck", cards=cards, deck=deck)
77
+
78
+
79
+ @deck_mcp.tool(
80
+ name="saveDeckConfig",
81
+ description="Saves the given configuration group. Returns true on success, false otherwise.",
82
+ )
83
+ async def save_deck_config_tool(
84
+ config: Annotated[
85
+ Dict[str, Any],
86
+ Field(
87
+ description="The deck configuration object to save. Must include an 'id'."
88
+ ),
89
+ ],
90
+ ) -> bool:
91
+ return await anki_call("saveDeckConfig", config=config)
@@ -0,0 +1,91 @@
1
+ from typing import Annotated, Any, Dict, List, Optional
2
+
3
+ from fastmcp import FastMCP
4
+ from pydantic import Field
5
+
6
+ from .common import anki_call
7
+
8
+ media_mcp = FastMCP(name="AnkiMediaService")
9
+
10
+
11
+ @media_mcp.tool(
12
+ name="retrieveMediaFile",
13
+ description="Retrieves the base64-encoded contents of the specified media file. Returns the base64 string or false if not found.",
14
+ )
15
+ async def retrieve_media_file_tool(
16
+ filename: Annotated[
17
+ str, Field(description="The name of the media file in Anki's collection.")
18
+ ],
19
+ ) -> Any:
20
+ return await anki_call("retrieveMediaFile", filename=filename)
21
+
22
+
23
+ @media_mcp.tool(
24
+ name="getMediaFilesNames",
25
+ description="Gets the names of media files matching the glob pattern. Returns a list of filenames.",
26
+ )
27
+ async def list_media_files_names_tool(
28
+ pattern: Annotated[
29
+ str, Field(description="A glob pattern to match filenames (e.g., '*.jpg').")
30
+ ],
31
+ ) -> List[str]:
32
+ return await anki_call("getMediaFilesNames", pattern=pattern)
33
+
34
+
35
+ @media_mcp.tool(
36
+ name="storeMediaFile",
37
+ description="Stores a media file in Anki's media folder. Provide one of 'data' (base64), 'path', or 'url'. Returns the stored filename or false on error.",
38
+ )
39
+ async def store_media_file_tool(
40
+ filename: Annotated[
41
+ str, Field(description="The desired filename in Anki's media collection.")
42
+ ],
43
+ data: Annotated[
44
+ Optional[str], Field(description="Base64-encoded file content.")
45
+ ] = None,
46
+ path: Annotated[
47
+ Optional[str], Field(description="Absolute local path to the file.")
48
+ ] = None,
49
+ url: Annotated[
50
+ Optional[str], Field(description="URL to download the file from.")
51
+ ] = None,
52
+ deleteExisting: Annotated[
53
+ Optional[bool],
54
+ Field(
55
+ description="Whether to delete an existing file with the same name. Default is true."
56
+ ),
57
+ ] = True,
58
+ ) -> Any:
59
+ params: Dict[str, Any] = {"filename": filename}
60
+ source_count = sum(1 for src in (data, path, url) if src is not None)
61
+ if source_count != 1:
62
+ raise ValueError(
63
+ "Exactly one of 'data', 'path', or 'url' must be provided for storeMediaFile."
64
+ )
65
+
66
+ if data:
67
+ params["data"] = data
68
+ elif path:
69
+ params["path"] = path
70
+ elif url:
71
+ params["url"] = url
72
+
73
+ if (
74
+ deleteExisting is not None
75
+ ):
76
+ params["deleteExisting"] = deleteExisting
77
+
78
+ return await anki_call("storeMediaFile", **params)
79
+
80
+
81
+ @media_mcp.tool(
82
+ name="deleteMediaFile",
83
+ description="Deletes the specified file from Anki's media folder.",
84
+ )
85
+ async def delete_media_file_tool(
86
+ filename: Annotated[
87
+ str,
88
+ Field(description="The name of the file to delete from the media collection."),
89
+ ],
90
+ ) -> None:
91
+ return await anki_call("deleteMediaFile", filename=filename)
@@ -0,0 +1,151 @@
1
+ from typing import Annotated, Any, Dict, List, Optional
2
+
3
+ from fastmcp import FastMCP
4
+ from pydantic import Field
5
+
6
+ from .common import anki_call
7
+
8
+ model_mcp = FastMCP(name="AnkiModelService")
9
+
10
+
11
+ @model_mcp.tool(
12
+ name="modelNamesAndIds",
13
+ description="Gets the complete list of model (note type) names and their IDs. Returns a dictionary mapping model names to IDs.",
14
+ )
15
+ async def list_model_names_and_ids_tool() -> Dict[str, int]:
16
+ return await anki_call("modelNamesAndIds")
17
+
18
+
19
+ @model_mcp.tool(
20
+ name="findModelsByName",
21
+ description="Gets a list of model definitions for the provided model names.",
22
+ )
23
+ async def find_models_by_name_tool(
24
+ modelNames: Annotated[List[str], Field(description="A list of model names.")],
25
+ ) -> List[Dict[str, Any]]:
26
+ return await anki_call("findModelsByName", modelNames=modelNames)
27
+
28
+
29
+ @model_mcp.tool(
30
+ name="modelFieldNames",
31
+ description="Gets the list of field names for the provided model name.",
32
+ )
33
+ async def get_model_field_names_tool(
34
+ modelName: Annotated[str, Field(description="The name of the model.")],
35
+ ) -> List[str]:
36
+ return await anki_call("modelFieldNames", modelName=modelName)
37
+
38
+
39
+ @model_mcp.tool(
40
+ name="modelTemplates",
41
+ description="Returns an object indicating the template content for each card of the specified model.",
42
+ )
43
+ async def get_model_templates_tool(
44
+ modelName: Annotated[str, Field(description="The name of the model.")],
45
+ ) -> Dict[str, Any]:
46
+ return await anki_call("modelTemplates", modelName=modelName)
47
+
48
+
49
+ @model_mcp.tool(
50
+ name="modelStyling",
51
+ description="Gets the CSS styling for the provided model name. Returns an object containing the 'css' field.",
52
+ )
53
+ async def get_model_styling_tool(
54
+ modelName: Annotated[str, Field(description="The name of the model.")],
55
+ ) -> Dict[str, Any]:
56
+ return await anki_call("modelStyling", modelName=modelName)
57
+
58
+
59
+ @model_mcp.tool(
60
+ name="createModel",
61
+ description="Creates a new model (note type). Returns the created model object.",
62
+ )
63
+ async def create_model_tool(
64
+ modelName: Annotated[str, Field(description="The name for the new model.")],
65
+ inOrderFields: Annotated[
66
+ List[str], Field(description="List of field names in order.")
67
+ ],
68
+ cardTemplates: Annotated[
69
+ List[Dict[str, Any]],
70
+ Field(
71
+ description="List of card template definitions. Each dict needs 'Name', 'Front', 'Back'."
72
+ ),
73
+ ],
74
+ css: Annotated[
75
+ Optional[str], Field(description="Optional CSS for the model.")
76
+ ] = None,
77
+ isCloze: Annotated[
78
+ Optional[bool], Field(description="Set to true if this is a Cloze model.")
79
+ ] = False,
80
+ modelId: Annotated[
81
+ Optional[int], Field(description="Optional model ID to use.")
82
+ ] = None,
83
+ ) -> Dict[str, Any]:
84
+ params: Dict[str, Any] = {
85
+ "modelName": modelName,
86
+ "inOrderFields": inOrderFields,
87
+ "cardTemplates": cardTemplates,
88
+ "isCloze": isCloze,
89
+ }
90
+ if css is not None:
91
+ params["css"] = css
92
+ if modelId is not None:
93
+ params["modelId"] = modelId
94
+ return await anki_call("createModel", **params)
95
+
96
+
97
+ @model_mcp.tool(
98
+ name="updateModelTemplates",
99
+ description="Modifies the templates of an existing model by name.",
100
+ )
101
+ async def update_model_templates_tool(
102
+ model: Annotated[
103
+ Dict[str, Any],
104
+ Field(
105
+ description="Model object. Must include 'name' (model name) and 'templates' (dict of template name to Front/Back definitions)."
106
+ ),
107
+ ],
108
+ ) -> None:
109
+ return await anki_call("updateModelTemplates", model=model)
110
+
111
+
112
+ @model_mcp.tool(
113
+ name="updateModelStyling",
114
+ description="Modifies the CSS styling of an existing model by name.",
115
+ )
116
+ async def update_model_styling_tool(
117
+ model: Annotated[
118
+ Dict[str, Any],
119
+ Field(
120
+ description="Model object. Must include 'name' (model name) and 'css' (the new CSS string)."
121
+ ),
122
+ ],
123
+ ) -> None:
124
+ return await anki_call("updateModelStyling", model=model)
125
+
126
+
127
+ @model_mcp.tool(
128
+ name="modelFieldAdd", description="Adds a new field to an existing model."
129
+ )
130
+ async def add_model_field_tool(
131
+ modelName: Annotated[str, Field(description="Name of the model to modify.")],
132
+ fieldName: Annotated[str, Field(description="Name of the new field to add.")],
133
+ index: Annotated[
134
+ Optional[int],
135
+ Field(description="Optional 0-based index to insert the field at."),
136
+ ] = None,
137
+ ) -> None:
138
+ params: Dict[str, Any] = {"modelName": modelName, "fieldName": fieldName}
139
+ if index is not None:
140
+ params["index"] = index
141
+ return await anki_call("modelFieldAdd", **params)
142
+
143
+
144
+ @model_mcp.tool(
145
+ name="modelFieldRemove", description="Removes a field from an existing model."
146
+ )
147
+ async def remove_model_field_tool(
148
+ modelName: Annotated[str, Field(description="Name of the model to modify.")],
149
+ fieldName: Annotated[str, Field(description="Name of the field to remove.")],
150
+ ) -> None:
151
+ return await anki_call("modelFieldRemove", modelName=modelName, fieldName=fieldName)
@@ -0,0 +1,130 @@
1
+ from typing import Annotated, Any, Dict, List, Optional
2
+
3
+ from fastmcp import FastMCP
4
+ from pydantic import Field
5
+
6
+ from .common import anki_call
7
+
8
+ note_mcp = FastMCP(name="AnkiNoteService")
9
+
10
+
11
+ @note_mcp.tool(
12
+ name="findNotes",
13
+ description="Returns an array of note IDs for a given Anki search query.",
14
+ )
15
+ async def find_notes_tool(
16
+ query: Annotated[
17
+ str, Field(description="Anki search query (e.g., 'deck:current card:1').")
18
+ ],
19
+ ) -> List[int]:
20
+ return await anki_call("findNotes", query=query)
21
+
22
+
23
+ @note_mcp.tool(
24
+ name="notesInfo",
25
+ description="Returns a list of objects containing information for each note ID provided.",
26
+ )
27
+ async def get_notes_info_tool(
28
+ notes: Annotated[List[int], Field(description="A list of note IDs.")],
29
+ ) -> List[Dict[str, Any]]:
30
+ return await anki_call("notesInfo", notes=notes)
31
+
32
+
33
+ @note_mcp.tool(
34
+ name="getNoteTags",
35
+ description="Gets the tags for a specific note ID. Returns a list of tags.",
36
+ )
37
+ async def get_note_tags_tool(
38
+ note: Annotated[int, Field(description="The ID of the note.")],
39
+ ) -> List[str]:
40
+ return await anki_call("getNoteTags", note=note)
41
+
42
+
43
+ @note_mcp.tool(
44
+ name="addNote",
45
+ description="Creates a new note using the given deck, model, fields, and tags. Returns the ID of the created note or null if the note could not be created.",
46
+ )
47
+ async def add_note_tool(
48
+ note: Annotated[
49
+ Dict[str, Any],
50
+ Field(
51
+ description="A dictionary representing the note to add. Should include 'deckName', 'modelName', 'fields', and optionally 'tags', 'options', 'audio', 'video', 'picture'."
52
+ ),
53
+ ],
54
+ ) -> Optional[int]:
55
+ return await anki_call("addNote", note=note)
56
+
57
+
58
+ @note_mcp.tool(
59
+ name="updateNoteFields", description="Modifies the fields of an existing note."
60
+ )
61
+ async def update_note_fields_tool(
62
+ note: Annotated[
63
+ Dict[str, Any],
64
+ Field(
65
+ description="A dictionary representing the note to update. Must include 'id' and 'fields'. Optionally 'audio', 'video', or 'picture'."
66
+ ),
67
+ ],
68
+ ) -> None:
69
+ return await anki_call("updateNoteFields", note=note)
70
+
71
+
72
+ @note_mcp.tool(name="deleteNotes", description="Deletes notes with the given IDs.")
73
+ async def delete_notes_tool(
74
+ notes: Annotated[List[int], Field(description="A list of note IDs to delete.")],
75
+ ) -> None:
76
+ return await anki_call("deleteNotes", notes=notes)
77
+
78
+
79
+ @note_mcp.tool(
80
+ name="addNotes",
81
+ description="Creates multiple notes. See 'addNote' for the structure of each note object in the list. Returns a list of new note IDs, or null for notes that couldn't be created.",
82
+ )
83
+ async def add_notes_tool(
84
+ notes: Annotated[
85
+ List[Dict[str, Any]], Field(description="A list of note objects to add.")
86
+ ],
87
+ ) -> List[Optional[int]]:
88
+ return await anki_call("addNotes", notes=notes)
89
+
90
+
91
+ @note_mcp.tool(name="addTags", description="Adds tags to the specified notes.")
92
+ async def add_tags_tool(
93
+ notes: Annotated[
94
+ List[int], Field(description="A list of note IDs to add tags to.")
95
+ ],
96
+ tags: Annotated[
97
+ str,
98
+ Field(
99
+ description="A space-separated string of tags to add (e.g., 'tag1 tag2')."
100
+ ),
101
+ ],
102
+ ) -> None:
103
+ return await anki_call("addTags", notes=notes, tags=tags)
104
+
105
+
106
+ @note_mcp.tool(name="removeTags", description="Removes tags from the specified notes.")
107
+ async def remove_tags_tool(
108
+ notes: Annotated[
109
+ List[int], Field(description="A list of note IDs to remove tags from.")
110
+ ],
111
+ tags: Annotated[
112
+ str, Field(description="A space-separated string of tags to remove.")
113
+ ],
114
+ ) -> None:
115
+ return await anki_call("removeTags", notes=notes, tags=tags)
116
+
117
+
118
+ @note_mcp.tool(
119
+ name="updateNote",
120
+ description="Modifies the fields and/or tags of an existing note.",
121
+ )
122
+ async def update_note_tool(
123
+ note: Annotated[
124
+ Dict[str, Any],
125
+ Field(
126
+ description="Note object to update. Must include 'id'. Can include 'fields', 'tags', 'audio', 'video', 'picture'."
127
+ ),
128
+ ],
129
+ ) -> None:
130
+ return await anki_call("updateNote", note=note)
@@ -0,0 +1,150 @@
1
+ Metadata-Version: 2.4
2
+ Name: iflow-mcp_ujisati_anki-mcp
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author-email: ujisati <98663233+ujisati@users.noreply.github.com>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: fastmcp>=2.3.3
9
+ Requires-Dist: httpx>=0.28.1
10
+ Description-Content-Type: text/markdown
11
+
12
+ # anki-mcp
13
+
14
+ A Model Context Protocol (MCP) server for interacting with Anki flashcards via the AnkiConnect add-on. This server exposes AnkiConnect actions as MCP tools, organized into logical services.
15
+
16
+ ## Prerequisites
17
+
18
+ - Anki desktop application
19
+ - AnkiConnect add-on installed and configured in Anki
20
+ - Python 3.8+
21
+ - `uv` (for running and installing dependencies, optional but recommended)
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ # Clone the repository
27
+ git clone https://github.com/ujisati/anki-mcp.git
28
+ cd anki-mcp
29
+
30
+ # Install dependencies (using uv)
31
+ uv pip install -e .
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ To run the MCP server:
37
+
38
+ ```bash
39
+ uv run anki-mcp
40
+ ```
41
+
42
+ The server will start and listen for MCP requests, typically interfacing with AnkiConnect at `http://127.0.0.1:8765`.
43
+
44
+ ### Inspecting the Server
45
+
46
+ You can use the MCP Inspector to view the available tools:
47
+
48
+ ```bash
49
+ npx @modelcontextprotocol/inspector uv run anki-mcp
50
+ ```
51
+
52
+ ## Configuration for MCP Clients
53
+
54
+ If you're integrating this with an MCP client (like an AI assistant framework), you'll need to configure it to find this server. Here's an example configuration snippet:
55
+
56
+ ```json
57
+ {
58
+ "mcpServers": {
59
+ "anki": {
60
+ "command": "uv",
61
+ "args": [
62
+ "run", // uv will find anki-mcp if run from project root
63
+ "anki-mcp"
64
+ ],
65
+ // If running from outside the project directory, specify the path:
66
+ // "args": [
67
+ // "--directory",
68
+ // "/ABSOLUTE/PATH/TO/anki-mcp", // Replace with actual path
69
+ // "run",
70
+ // "anki-mcp"
71
+ // ]
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## Available MCP Tools
78
+
79
+ This MCP server provides access to Anki functionality through tools grouped by services. The tool names correspond directly to AnkiConnect actions.
80
+
81
+ ### Deck Service (`deck.*`)
82
+ - **`deck.deckNamesAndIds`**: Gets the complete list of deck names and their respective IDs.
83
+ - **`deck.getDeckConfig`**: Gets the configuration group object for a given deck name.
84
+ - **`deck.deckNames`**: Gets the complete list of deck names for the current user.
85
+ - **`deck.createDeck`**: Creates a new empty deck.
86
+ - **`deck.deleteDecks`**: Deletes specified decks.
87
+ - **`deck.changeDeck`**: Moves cards to a different deck.
88
+ - **`deck.saveDeckConfig`**: Saves a deck configuration group.
89
+
90
+ ### Note Service (`note.*`)
91
+ - **`note.findNotes`**: Returns note IDs for a given Anki search query.
92
+ - **`note.notesInfo`**: Returns information for specified note IDs.
93
+ - **`note.getNoteTags`**: Gets the tags for a specific note ID.
94
+ - **`note.addNote`**: Creates a new note.
95
+ - **`note.updateNoteFields`**: Modifies the fields of an existing note.
96
+ - **`note.deleteNotes`**: Deletes specified notes.
97
+ - **`note.addNotes`**: Creates multiple notes.
98
+ - **`note.addTags`**: Adds tags to specified notes.
99
+ - **`note.removeTags`**: Removes tags from specified notes.
100
+ - **`note.updateNote`**: Modifies the fields and/or tags of an existing note.
101
+
102
+ ### Card Service (`card.*`)
103
+ - **`card.findCards`**: Returns card IDs for a given Anki search query.
104
+ - **`card.cardsInfo`**: Returns information for specified card IDs.
105
+ - **`card.cardsToNotes`**: Returns note IDs for given card IDs.
106
+ - **`card.areSuspended`**: Checks if specified cards are suspended.
107
+ - **`card.cardsModTime`**: Returns modification time for specified card IDs.
108
+ - **`card.suspended`**: Checks if a single card is suspended.
109
+ - **`card.suspend`**: Suspends specified cards.
110
+ - **`card.unsuspend`**: Unsuspends specified cards.
111
+ - **`card.setSpecificValueOfCard`**: Sets specific values of a single card (use with caution).
112
+
113
+ ### Model Service (`model.*`) (Note Types)
114
+ - **`model.modelNamesAndIds`**: Gets the complete list of model (note type) names and their IDs.
115
+ - **`model.findModelsByName`**: Gets model definitions for provided model names.
116
+ - **`model.modelFieldNames`**: Gets field names for a given model name.
117
+ - **`model.modelTemplates`**: Gets template content for each card of a specified model.
118
+ - **`model.modelStyling`**: Gets CSS styling for a given model name.
119
+ - **`model.createModel`**: Creates a new model (note type).
120
+ - **`model.updateModelTemplates`**: Modifies templates of an existing model.
121
+ - **`model.updateModelStyling`**: Modifies CSS styling of an existing model.
122
+ - **`model.modelFieldAdd`**: Adds a new field to an existing model.
123
+ - **`model.modelFieldRemove`**: Removes a field from an existing model.
124
+
125
+ ### Media Service (`media.*`)
126
+ - **`media.retrieveMediaFile`**: Retrieves the base64-encoded contents of a media file.
127
+ - **`media.getMediaFilesNames`**: Gets names of media files matching a glob pattern.
128
+ - **`media.storeMediaFile`**: Stores a media file (from base64, path, or URL).
129
+ - **`media.deleteMediaFile`**: Deletes a specified media file.
130
+
131
+ ## Development
132
+
133
+ To set up for development:
134
+
135
+ ```bash
136
+ uv sync
137
+ source .venv/bin/activate
138
+
139
+ uv pip install -e .
140
+ ```
141
+
142
+ ### Running Tests
143
+
144
+ ```bash
145
+ pytest
146
+ ```
147
+
148
+ ## Todo
149
+
150
+ - [ ] Finish adding all AnkiConnect tools
@@ -0,0 +1,12 @@
1
+ anki_mcp/__init__.py,sha256=koEUwXZFjc0ZKJFlWMTDIrnLSb7EBiLtMdy3YU_icF0,1009
2
+ anki_mcp/card_service.py,sha256=b-nmzymS1_5UrR_0dYZq9wu9sE5zE6qpzRmmHYvOW8s,3926
3
+ anki_mcp/common.py,sha256=mugKG_69xcFJGyT05MN-BDhA4u73zC1zSMDTsJHWw1c,1028
4
+ anki_mcp/deck_service.py,sha256=M7CwD_08w-n0hgrhw21VRnPLi9KlCD8riB60hOW-ky4,2899
5
+ anki_mcp/media_service.py,sha256=JeRgwgN0a-97J-TS39cMv1CteJcr5fZiiekNJ9P3G5c,3074
6
+ anki_mcp/model_service.py,sha256=J3dQ-jwygrih8WE8L-aE2Wf1IUORWeIoP-iCOiV0knk,5256
7
+ anki_mcp/note_service.py,sha256=-qGed8Z0iArMb_X7asX_qnGQs9gxpEKW8BE_hA-dSEM,4084
8
+ iflow_mcp_ujisati_anki_mcp-0.1.0.dist-info/METADATA,sha256=sqQPxCp-pzLMZpeu4yW9Ai8S5At1n6k8QB5x6GLjz6c,5255
9
+ iflow_mcp_ujisati_anki_mcp-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
10
+ iflow_mcp_ujisati_anki_mcp-0.1.0.dist-info/entry_points.txt,sha256=iwp_5h2sAepOxyMntoEj9WzVpCgnDHDLz7q3cOe6Fh8,43
11
+ iflow_mcp_ujisati_anki_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=rB7eke8ySHfrR9MppOeN1SPaAFHlTYrCIL0mpPDxh6A,1064
12
+ iflow_mcp_ujisati_anki_mcp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ anki-mcp = anki_mcp:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ujisati
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.