@spacholski1225/anki-connect-mcp 1.0.1
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.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/index.js +362 -0
- package/dist/methods/anki.js +22 -0
- package/dist/tools/cards.js +118 -0
- package/dist/tools/decks.js +139 -0
- package/dist/tools/graphical.js +133 -0
- package/dist/tools/media.js +48 -0
- package/dist/tools/misc.js +171 -0
- package/dist/tools/models.js +50 -0
- package/dist/tools/notes.js +120 -0
- package/dist/types/anki/base.js +12 -0
- package/dist/types/anki/cards.js +1 -0
- package/dist/types/anki/decks.js +1 -0
- package/dist/types/anki/index.js +9 -0
- package/dist/types/anki/media.js +1 -0
- package/dist/types/anki/misc.js +1 -0
- package/dist/types/anki/models.js +1 -0
- package/dist/types/anki/notes.js +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Szymon Pacholski
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# Anki Connect MCP Server
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server that provides seamless integration with Anki through the AnkiConnect add-on. This server enables AI assistants and other MCP clients to interact with your Anki flashcard collection programmatically.
|
|
4
|
+
|
|
5
|
+
## What is Anki Connect?
|
|
6
|
+
|
|
7
|
+
[AnkiConnect](https://ankiweb.net/shared/info/2055492159) is a powerful Anki add-on that exposes Anki's functionality through a REST API. It allows external applications to:
|
|
8
|
+
|
|
9
|
+
- Create, modify, and delete flashcards
|
|
10
|
+
- Manage decks and their configurations
|
|
11
|
+
- Control Anki's GUI programmatically
|
|
12
|
+
- Search and retrieve card information
|
|
13
|
+
- Manage tags and note types
|
|
14
|
+
- Perform bulk operations on cards and notes
|
|
15
|
+
|
|
16
|
+
## What is MCP (Model Context Protocol)?
|
|
17
|
+
|
|
18
|
+
The Model Context Protocol is a standardized way for AI assistants to connect to external data sources and tools. This MCP server acts as a bridge between MCP clients (like Claude Desktop) and AnkiConnect, allowing AI assistants to help you manage your Anki flashcards naturally through conversation.
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
This MCP server provides comprehensive access to AnkiConnect functionality through the following tools:
|
|
23
|
+
|
|
24
|
+
### Flashcard Management
|
|
25
|
+
- [`create_flashcard()`](src/index.ts:67) - Create new flashcards with front and back content
|
|
26
|
+
- [`add_note()`](src/tools/notes.ts:22) - Add notes with custom fields and tags
|
|
27
|
+
- [`add_notes()`](src/tools/notes.ts) - Bulk add multiple notes
|
|
28
|
+
- [`update_note_fields()`](src/tools/notes.ts) - Update existing note fields
|
|
29
|
+
- [`find_notes()`](src/tools/notes.ts) - Search for notes using Anki's query syntax
|
|
30
|
+
|
|
31
|
+
### Deck Management
|
|
32
|
+
- [`deck_names()`](src/tools/decks.ts:19) - Get all deck names
|
|
33
|
+
- [`deck_names_and_ids()`](src/tools/decks.ts:30) - Get deck names with their IDs
|
|
34
|
+
- [`change_deck()`](src/tools/decks.ts) - Move cards between decks
|
|
35
|
+
- [`delete_decks()`](src/tools/decks.ts) - Delete decks and optionally their cards
|
|
36
|
+
- [`get_deck_config()`](src/tools/decks.ts) - Retrieve deck configuration settings
|
|
37
|
+
|
|
38
|
+
### Card Operations
|
|
39
|
+
- [`find_cards()`](src/tools/cards.ts) - Search for cards
|
|
40
|
+
- [`cards_info()`](src/tools/cards.ts) - Get detailed card information
|
|
41
|
+
- [`suspend()`](src/tools/cards.ts:18) / [`unsuspend()`](src/tools/cards.ts) - Suspend/unsuspend cards
|
|
42
|
+
- [`are_suspended()`](src/tools/cards.ts) / [`are_due()`](src/tools/cards.ts) - Check card status
|
|
43
|
+
|
|
44
|
+
### GUI Control
|
|
45
|
+
- [`gui_deck_browser()`](src/index.ts:177) - Open deck browser
|
|
46
|
+
- [`gui_deck_review()`](src/index.ts:185) - Start deck review
|
|
47
|
+
- [`gui_browse()`](src/index.ts:196) - Open card browser with search
|
|
48
|
+
- [`gui_add_cards()`](src/index.ts:207) - Open add cards dialog
|
|
49
|
+
|
|
50
|
+
### Utility Tools
|
|
51
|
+
- [`version()`](src/index.ts:98) - Get AnkiConnect version
|
|
52
|
+
- [`get_tags()`](src/index.ts:114) - Retrieve all tags
|
|
53
|
+
- [`multi()`](src/index.ts:223) - Execute multiple AnkiConnect requests in batch
|
|
54
|
+
|
|
55
|
+
## Prerequisites
|
|
56
|
+
|
|
57
|
+
### 1. Install AnkiConnect Add-on
|
|
58
|
+
|
|
59
|
+
1. Open Anki
|
|
60
|
+
2. Go to **Tools** → **Add-ons**
|
|
61
|
+
3. Click **Get Add-ons...**
|
|
62
|
+
4. Enter the code: `2055492159`
|
|
63
|
+
5. Click **OK** and restart Anki
|
|
64
|
+
|
|
65
|
+
### 2. Configure AnkiConnect (Optional)
|
|
66
|
+
|
|
67
|
+
AnkiConnect works out of the box, but you can customize its settings:
|
|
68
|
+
|
|
69
|
+
1. Go to **Tools** → **Add-ons**
|
|
70
|
+
2. Select **AnkiConnect** and click **Config**
|
|
71
|
+
3. Modify settings if needed (default port is 8765)
|
|
72
|
+
4. Restart Anki after making changes
|
|
73
|
+
|
|
74
|
+
## Installation
|
|
75
|
+
|
|
76
|
+
1. Clone this repository:
|
|
77
|
+
```bash
|
|
78
|
+
git clone https://github.com/spacholski1225/mcp-server.git
|
|
79
|
+
cd mcp-server
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
2. Install dependencies:
|
|
83
|
+
```bash
|
|
84
|
+
npm install
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
3. Build the project:
|
|
88
|
+
```bash
|
|
89
|
+
npm run build
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## MCP Client Configuration
|
|
93
|
+
|
|
94
|
+
### Claude Desktop
|
|
95
|
+
|
|
96
|
+
To use this MCP server with Claude Desktop, add the following configuration to your MCP settings file:
|
|
97
|
+
|
|
98
|
+
**Location of MCP configuration file:**
|
|
99
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
100
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
101
|
+
- **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
|
102
|
+
|
|
103
|
+
**Configuration:**
|
|
104
|
+
```json
|
|
105
|
+
{
|
|
106
|
+
"mcpServers": {
|
|
107
|
+
"anki-connect": {
|
|
108
|
+
"command": "node",
|
|
109
|
+
"args": [
|
|
110
|
+
"dist/index.js"
|
|
111
|
+
],
|
|
112
|
+
"cwd": "/path/to/your/mcp-server",
|
|
113
|
+
"env": {},
|
|
114
|
+
"disabled": false,
|
|
115
|
+
"alwaysAllow": [
|
|
116
|
+
"deck_names",
|
|
117
|
+
"cards_info",
|
|
118
|
+
"deck_names_and_ids",
|
|
119
|
+
"gui_deck_browser"
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Important Notes:**
|
|
127
|
+
- Replace `/path/to/your/mcp-server` with the actual path to your cloned repository
|
|
128
|
+
- The `alwaysAllow` array contains tools that won't require confirmation each time
|
|
129
|
+
- Make sure Anki is running with AnkiConnect enabled before using the MCP server
|
|
130
|
+
|
|
131
|
+
### Other MCP Clients
|
|
132
|
+
|
|
133
|
+
For other MCP clients, use the following connection details:
|
|
134
|
+
- **Transport**: stdio
|
|
135
|
+
- **Command**: `node dist/index.js`
|
|
136
|
+
- **Working Directory**: Path to this repository
|
|
137
|
+
|
|
138
|
+
## Usage Examples
|
|
139
|
+
|
|
140
|
+
Once configured with an MCP client like Claude Desktop, you can interact with your Anki collection naturally:
|
|
141
|
+
|
|
142
|
+
- *"Create a flashcard about the capital of France"*
|
|
143
|
+
- *"Show me all my deck names"*
|
|
144
|
+
- *"Find all cards tagged with 'vocabulary'"*
|
|
145
|
+
- *"Suspend all cards in the 'Difficult Words' deck"*
|
|
146
|
+
- *"Open the deck browser in Anki"*
|
|
147
|
+
|
|
148
|
+
## Development
|
|
149
|
+
|
|
150
|
+
### Project Structure
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
src/
|
|
154
|
+
├── index.ts # Main MCP server setup and tool definitions
|
|
155
|
+
├── methods/
|
|
156
|
+
│ └── anki.ts # AnkiConnect API communication
|
|
157
|
+
├── tools/ # Tool implementations
|
|
158
|
+
│ ├── cards.ts # Card-related operations
|
|
159
|
+
│ ├── decks.ts # Deck management
|
|
160
|
+
│ ├── misc.ts # Utility functions and GUI controls
|
|
161
|
+
│ ├── models.ts # Note type operations
|
|
162
|
+
│ └── notes.ts # Note management
|
|
163
|
+
└── types/ # TypeScript type definitions
|
|
164
|
+
└── anki/ # AnkiConnect API types
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Available Scripts
|
|
168
|
+
|
|
169
|
+
- `npm run build` - Compile TypeScript to JavaScript
|
|
170
|
+
- `npm start` - Run the compiled MCP server
|
|
171
|
+
- `npm test` - Run tests (not implemented yet)
|
|
172
|
+
|
|
173
|
+
## Troubleshooting
|
|
174
|
+
|
|
175
|
+
### Common Issues
|
|
176
|
+
|
|
177
|
+
1. **Connection refused**: Make sure Anki is running and AnkiConnect is installed
|
|
178
|
+
2. **Port conflicts**: Check if port 8765 is available or configure AnkiConnect to use a different port
|
|
179
|
+
3. **Permission errors**: Ensure the MCP server has proper file system permissions
|
|
180
|
+
4. **Tool not found**: Verify the MCP client configuration and restart the client
|
|
181
|
+
|
|
182
|
+
### Debug Mode
|
|
183
|
+
|
|
184
|
+
To enable debug logging, check the console output when running the MCP server. All AnkiConnect requests and responses are logged for troubleshooting.
|
|
185
|
+
|
|
186
|
+
## Contributing
|
|
187
|
+
|
|
188
|
+
Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
ISC License
|
|
193
|
+
|
|
194
|
+
## References
|
|
195
|
+
|
|
196
|
+
- [AnkiConnect Documentation](https://foosoft.net/projects/anki-connect/)
|
|
197
|
+
- [Model Context Protocol Specification](https://modelcontextprotocol.io/)
|
|
198
|
+
- [MCP SDK Documentation](https://github.com/modelcontextprotocol/typescript-sdk)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { callAnkiConnect } from "./methods/anki.js";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { getVersion, getTags, guiCurrentCard, guiStartCardTimer, guiShowQuestion, guiShowAnswer, guiAnswerCard, guiDeckOverview, guiDeckBrowser, guiDeckReview, guiBrowse, guiAddCards, guiExitAnki, multi, upgrade } from "./tools/misc.js";
|
|
7
|
+
import { deckNames, deckNamesAndIds, getDecks, changeDeck, deleteDecks, getDeckConfig, saveDeckConfig, setDeckConfigId, cloneDeckConfigId, removeDeckConfigId } from "./tools/decks.js";
|
|
8
|
+
import { modelNames, modelNamesAndIds, modelFieldNames, modelFieldsOnTemplates } from "./tools/models.js";
|
|
9
|
+
import { addNote, addNotes, canAddNotes, updateNoteFields, addTags, removeTags, findNotes, notesInfo } from "./tools/notes.js";
|
|
10
|
+
import { suspend, unsuspend, areSuspended, areDue, getIntervals, findCards, cardsToNotes, cardsInfo } from "./tools/cards.js";
|
|
11
|
+
const server = new McpServer({
|
|
12
|
+
name: 'anki-connect mcp',
|
|
13
|
+
version: '0.0.1'
|
|
14
|
+
});
|
|
15
|
+
server.tool('create_flashcard', {
|
|
16
|
+
front: z.string(),
|
|
17
|
+
back: z.string()
|
|
18
|
+
}, async ({ front, back }) => {
|
|
19
|
+
const mcpFlashCard = {
|
|
20
|
+
action: "addNote",
|
|
21
|
+
version: 6,
|
|
22
|
+
params: {
|
|
23
|
+
note: {
|
|
24
|
+
deckName: "Default",
|
|
25
|
+
modelName: "Basic",
|
|
26
|
+
fields: {
|
|
27
|
+
Front: front,
|
|
28
|
+
Back: back
|
|
29
|
+
},
|
|
30
|
+
tags: ["ai"],
|
|
31
|
+
options: {
|
|
32
|
+
allowDuplicate: false
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const responseData = await callAnkiConnect(mcpFlashCard);
|
|
38
|
+
return { content: [{ type: "text", text: JSON.stringify(responseData) }] };
|
|
39
|
+
});
|
|
40
|
+
// Version and basic info tools
|
|
41
|
+
server.tool('version', async () => {
|
|
42
|
+
const response = await getVersion();
|
|
43
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
44
|
+
});
|
|
45
|
+
server.tool('upgrade', async () => {
|
|
46
|
+
const response = await upgrade();
|
|
47
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
48
|
+
});
|
|
49
|
+
server.tool('get_tags', async () => {
|
|
50
|
+
const response = await getTags();
|
|
51
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
52
|
+
});
|
|
53
|
+
// GUI control tools
|
|
54
|
+
server.tool('gui_current_card', async () => {
|
|
55
|
+
const response = await guiCurrentCard();
|
|
56
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
57
|
+
});
|
|
58
|
+
server.tool('gui_start_card_timer', async () => {
|
|
59
|
+
const response = await guiStartCardTimer();
|
|
60
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
61
|
+
});
|
|
62
|
+
server.tool('gui_show_question', async () => {
|
|
63
|
+
const response = await guiShowQuestion();
|
|
64
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
65
|
+
});
|
|
66
|
+
server.tool('gui_show_answer', async () => {
|
|
67
|
+
const response = await guiShowAnswer();
|
|
68
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
69
|
+
});
|
|
70
|
+
server.tool('gui_answer_card', {
|
|
71
|
+
ease: z.number().min(1).max(4).describe('Answer ease: 1=Again, 2=Hard, 3=Good, 4=Easy')
|
|
72
|
+
}, async ({ ease }) => {
|
|
73
|
+
const response = await guiAnswerCard(ease);
|
|
74
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
75
|
+
});
|
|
76
|
+
server.tool('gui_deck_overview', {
|
|
77
|
+
name: z.string().describe('Name of the deck to open')
|
|
78
|
+
}, async ({ name }) => {
|
|
79
|
+
const response = await guiDeckOverview(name);
|
|
80
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
81
|
+
});
|
|
82
|
+
server.tool('gui_deck_browser', async () => {
|
|
83
|
+
const response = await guiDeckBrowser();
|
|
84
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
85
|
+
});
|
|
86
|
+
server.tool('gui_deck_review', {
|
|
87
|
+
name: z.string().describe('Name of the deck to review')
|
|
88
|
+
}, async ({ name }) => {
|
|
89
|
+
const response = await guiDeckReview(name);
|
|
90
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
91
|
+
});
|
|
92
|
+
server.tool('gui_browse', {
|
|
93
|
+
query: z.string().describe('Search query for browsing cards')
|
|
94
|
+
}, async ({ query }) => {
|
|
95
|
+
const response = await guiBrowse(query);
|
|
96
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
97
|
+
});
|
|
98
|
+
server.tool('gui_add_cards', async () => {
|
|
99
|
+
const response = await guiAddCards();
|
|
100
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
101
|
+
});
|
|
102
|
+
server.tool('gui_exit_anki', async () => {
|
|
103
|
+
const response = await guiExitAnki();
|
|
104
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
105
|
+
});
|
|
106
|
+
server.tool('multi', {
|
|
107
|
+
actions: z.array(z.object({
|
|
108
|
+
action: z.string(),
|
|
109
|
+
version: z.number(),
|
|
110
|
+
params: z.record(z.any()).optional()
|
|
111
|
+
})).describe('Array of AnkiConnect requests to execute')
|
|
112
|
+
}, async ({ actions }) => {
|
|
113
|
+
const response = await multi(actions);
|
|
114
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
115
|
+
});
|
|
116
|
+
// Deck management tools
|
|
117
|
+
server.tool('deck_names', async () => {
|
|
118
|
+
const response = await deckNames();
|
|
119
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
120
|
+
});
|
|
121
|
+
server.tool('deck_names_and_ids', async () => {
|
|
122
|
+
const response = await deckNamesAndIds();
|
|
123
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
124
|
+
});
|
|
125
|
+
server.tool('get_decks', {
|
|
126
|
+
cards: z.array(z.number()).describe('Array of card IDs to get deck information for')
|
|
127
|
+
}, async ({ cards }) => {
|
|
128
|
+
const response = await getDecks(cards);
|
|
129
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
130
|
+
});
|
|
131
|
+
server.tool('change_deck', {
|
|
132
|
+
cards: z.array(z.number()).describe('Array of card IDs to move'),
|
|
133
|
+
deck: z.string().describe('Name of the target deck')
|
|
134
|
+
}, async ({ cards, deck }) => {
|
|
135
|
+
const response = await changeDeck(cards, deck);
|
|
136
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
137
|
+
});
|
|
138
|
+
server.tool('delete_decks', {
|
|
139
|
+
decks: z.array(z.string()).describe('Array of deck names to delete'),
|
|
140
|
+
cardsToo: z.boolean().optional().describe('If true, cards within deleted decks will also be deleted; otherwise moved to default deck')
|
|
141
|
+
}, async ({ decks, cardsToo }) => {
|
|
142
|
+
const response = await deleteDecks(decks, cardsToo);
|
|
143
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
144
|
+
});
|
|
145
|
+
server.tool('get_deck_config', {
|
|
146
|
+
deck: z.string().describe('Name of the deck to get configuration for')
|
|
147
|
+
}, async ({ deck }) => {
|
|
148
|
+
const response = await getDeckConfig(deck);
|
|
149
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
150
|
+
});
|
|
151
|
+
server.tool('save_deck_config', {
|
|
152
|
+
config: z.object({
|
|
153
|
+
lapse: z.object({
|
|
154
|
+
leechFails: z.number(),
|
|
155
|
+
delays: z.array(z.number()),
|
|
156
|
+
minInt: z.number(),
|
|
157
|
+
leechAction: z.number(),
|
|
158
|
+
mult: z.number()
|
|
159
|
+
}),
|
|
160
|
+
dyn: z.boolean(),
|
|
161
|
+
autoplay: z.boolean(),
|
|
162
|
+
mod: z.number(),
|
|
163
|
+
id: z.number(),
|
|
164
|
+
maxTaken: z.number(),
|
|
165
|
+
new: z.object({
|
|
166
|
+
bury: z.boolean(),
|
|
167
|
+
order: z.number(),
|
|
168
|
+
initialFactor: z.number(),
|
|
169
|
+
perDay: z.number(),
|
|
170
|
+
delays: z.array(z.number()),
|
|
171
|
+
separate: z.boolean(),
|
|
172
|
+
ints: z.array(z.number())
|
|
173
|
+
}),
|
|
174
|
+
name: z.string(),
|
|
175
|
+
rev: z.object({
|
|
176
|
+
bury: z.boolean(),
|
|
177
|
+
ivlFct: z.number(),
|
|
178
|
+
ease4: z.number(),
|
|
179
|
+
maxIvl: z.number(),
|
|
180
|
+
perDay: z.number(),
|
|
181
|
+
minSpace: z.number(),
|
|
182
|
+
fuzz: z.number()
|
|
183
|
+
}),
|
|
184
|
+
timer: z.number(),
|
|
185
|
+
replayq: z.boolean(),
|
|
186
|
+
usn: z.number()
|
|
187
|
+
}).describe('Deck configuration object to save')
|
|
188
|
+
}, async ({ config }) => {
|
|
189
|
+
const response = await saveDeckConfig(config);
|
|
190
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
191
|
+
});
|
|
192
|
+
server.tool('set_deck_config_id', {
|
|
193
|
+
decks: z.array(z.string()).describe('Array of deck names to change configuration for'),
|
|
194
|
+
configId: z.number().describe('Configuration group ID to assign')
|
|
195
|
+
}, async ({ decks, configId }) => {
|
|
196
|
+
const response = await setDeckConfigId(decks, configId);
|
|
197
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
198
|
+
});
|
|
199
|
+
server.tool('clone_deck_config_id', {
|
|
200
|
+
name: z.string().describe('Name for the new configuration group'),
|
|
201
|
+
cloneFrom: z.number().optional().describe('ID of the configuration group to clone from (optional, defaults to default group)')
|
|
202
|
+
}, async ({ name, cloneFrom }) => {
|
|
203
|
+
const response = await cloneDeckConfigId(name, cloneFrom);
|
|
204
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
205
|
+
});
|
|
206
|
+
server.tool('remove_deck_config_id', {
|
|
207
|
+
configId: z.number().describe('ID of the configuration group to remove')
|
|
208
|
+
}, async ({ configId }) => {
|
|
209
|
+
const response = await removeDeckConfigId(configId);
|
|
210
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
211
|
+
});
|
|
212
|
+
// Model management tools
|
|
213
|
+
server.tool('model_names', async () => {
|
|
214
|
+
const response = await modelNames();
|
|
215
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
216
|
+
});
|
|
217
|
+
server.tool('model_names_and_ids', async () => {
|
|
218
|
+
const response = await modelNamesAndIds();
|
|
219
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
220
|
+
});
|
|
221
|
+
server.tool('model_field_names', {
|
|
222
|
+
modelName: z.string().describe('Name of the model to get field names for')
|
|
223
|
+
}, async ({ modelName }) => {
|
|
224
|
+
const response = await modelFieldNames(modelName);
|
|
225
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
226
|
+
});
|
|
227
|
+
server.tool('model_fields_on_templates', {
|
|
228
|
+
modelName: z.string().describe('Name of the model to get template fields for')
|
|
229
|
+
}, async ({ modelName }) => {
|
|
230
|
+
const response = await modelFieldsOnTemplates(modelName);
|
|
231
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
232
|
+
});
|
|
233
|
+
server.tool('add_note', {
|
|
234
|
+
deckName: z.string().describe('Name of the deck to add the note to'),
|
|
235
|
+
modelName: z.string().describe('Name of the note model/type'),
|
|
236
|
+
fields: z.record(z.string()).describe('Object with field names as keys and field values as strings'),
|
|
237
|
+
tags: z.array(z.string()).optional().describe('Array of tags to add to the note'),
|
|
238
|
+
audio: z.object({
|
|
239
|
+
url: z.string(),
|
|
240
|
+
filename: z.string(),
|
|
241
|
+
skipHash: z.string().optional(),
|
|
242
|
+
fields: z.string()
|
|
243
|
+
}).optional().describe('Optional audio attachment')
|
|
244
|
+
}, async ({ deckName, modelName, fields, tags, audio }) => {
|
|
245
|
+
const note = { deckName, modelName, fields, tags, audio };
|
|
246
|
+
const response = await addNote(note);
|
|
247
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
248
|
+
});
|
|
249
|
+
server.tool('add_notes', {
|
|
250
|
+
notes: z.array(z.object({
|
|
251
|
+
deckName: z.string().describe('Name of the deck to add the note to'),
|
|
252
|
+
modelName: z.string().describe('Name of the note model/type'),
|
|
253
|
+
fields: z.record(z.string()).describe('Object with field names as keys and field values as strings'),
|
|
254
|
+
tags: z.array(z.string()).optional().describe('Array of tags to add to the note'),
|
|
255
|
+
audio: z.object({
|
|
256
|
+
url: z.string(),
|
|
257
|
+
filename: z.string(),
|
|
258
|
+
skipHash: z.string().optional(),
|
|
259
|
+
fields: z.string()
|
|
260
|
+
}).optional().describe('Optional audio attachment')
|
|
261
|
+
})).describe('Array of note objects to create')
|
|
262
|
+
}, async ({ notes }) => {
|
|
263
|
+
const response = await addNotes(notes);
|
|
264
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
265
|
+
});
|
|
266
|
+
server.tool('can_add_notes', {
|
|
267
|
+
notes: z.array(z.object({
|
|
268
|
+
deckName: z.string().describe('Name of the deck to add the note to'),
|
|
269
|
+
modelName: z.string().describe('Name of the note model/type'),
|
|
270
|
+
fields: z.record(z.string()).describe('Object with field names as keys and field values as strings'),
|
|
271
|
+
tags: z.array(z.string()).optional().describe('Array of tags to add to the note')
|
|
272
|
+
})).describe('Array of note objects to check')
|
|
273
|
+
}, async ({ notes }) => {
|
|
274
|
+
const response = await canAddNotes(notes);
|
|
275
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
276
|
+
});
|
|
277
|
+
server.tool('update_note_fields', {
|
|
278
|
+
id: z.number().describe('ID of the note to update'),
|
|
279
|
+
fields: z.record(z.string()).describe('Object with field names as keys and new field values as strings')
|
|
280
|
+
}, async ({ id, fields }) => {
|
|
281
|
+
const noteUpdate = { id, fields };
|
|
282
|
+
const response = await updateNoteFields(noteUpdate);
|
|
283
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
284
|
+
});
|
|
285
|
+
server.tool('add_tags', {
|
|
286
|
+
notes: z.array(z.number()).describe('Array of note IDs to add tags to'),
|
|
287
|
+
tags: z.string().describe('Space-separated string of tags to add')
|
|
288
|
+
}, async ({ notes, tags }) => {
|
|
289
|
+
const response = await addTags(notes, tags);
|
|
290
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
291
|
+
});
|
|
292
|
+
server.tool('remove_tags', {
|
|
293
|
+
notes: z.array(z.number()).describe('Array of note IDs to remove tags from'),
|
|
294
|
+
tags: z.string().describe('Space-separated string of tags to remove')
|
|
295
|
+
}, async ({ notes, tags }) => {
|
|
296
|
+
const response = await removeTags(notes, tags);
|
|
297
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
298
|
+
});
|
|
299
|
+
server.tool('find_notes', {
|
|
300
|
+
query: z.string().describe('Search query string (same syntax as Anki browser)')
|
|
301
|
+
}, async ({ query }) => {
|
|
302
|
+
const response = await findNotes(query);
|
|
303
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
304
|
+
});
|
|
305
|
+
server.tool('notes_info', {
|
|
306
|
+
notes: z.array(z.number()).describe('Array of note IDs to get information for')
|
|
307
|
+
}, async ({ notes }) => {
|
|
308
|
+
const response = await notesInfo(notes);
|
|
309
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
310
|
+
});
|
|
311
|
+
// Card management tools
|
|
312
|
+
server.tool('suspend', {
|
|
313
|
+
cards: z.array(z.number()).describe('Array of card IDs to suspend')
|
|
314
|
+
}, async ({ cards }) => {
|
|
315
|
+
const response = await suspend(cards);
|
|
316
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
317
|
+
});
|
|
318
|
+
server.tool('unsuspend', {
|
|
319
|
+
cards: z.array(z.number()).describe('Array of card IDs to unsuspend')
|
|
320
|
+
}, async ({ cards }) => {
|
|
321
|
+
const response = await unsuspend(cards);
|
|
322
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
323
|
+
});
|
|
324
|
+
server.tool('are_suspended', {
|
|
325
|
+
cards: z.array(z.number()).describe('Array of card IDs to check if suspended')
|
|
326
|
+
}, async ({ cards }) => {
|
|
327
|
+
const response = await areSuspended(cards);
|
|
328
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
329
|
+
});
|
|
330
|
+
server.tool('are_due', {
|
|
331
|
+
cards: z.array(z.number()).describe('Array of card IDs to check if due')
|
|
332
|
+
}, async ({ cards }) => {
|
|
333
|
+
const response = await areDue(cards);
|
|
334
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
335
|
+
});
|
|
336
|
+
server.tool('get_intervals', {
|
|
337
|
+
cards: z.array(z.number()).describe('Array of card IDs to get intervals for'),
|
|
338
|
+
complete: z.boolean().optional().describe('If true, returns all intervals; if false, returns only the most recent interval')
|
|
339
|
+
}, async ({ cards, complete }) => {
|
|
340
|
+
const response = await getIntervals(cards, complete);
|
|
341
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
342
|
+
});
|
|
343
|
+
server.tool('find_cards', {
|
|
344
|
+
query: z.string().describe('Search query string (same syntax as Anki browser)')
|
|
345
|
+
}, async ({ query }) => {
|
|
346
|
+
const response = await findCards(query);
|
|
347
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
348
|
+
});
|
|
349
|
+
server.tool('cards_to_notes', {
|
|
350
|
+
cards: z.array(z.number()).describe('Array of card IDs to convert to note IDs')
|
|
351
|
+
}, async ({ cards }) => {
|
|
352
|
+
const response = await cardsToNotes(cards);
|
|
353
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
354
|
+
});
|
|
355
|
+
server.tool('cards_info', {
|
|
356
|
+
cards: z.array(z.number()).describe('Array of card IDs to get detailed information for')
|
|
357
|
+
}, async ({ cards }) => {
|
|
358
|
+
const response = await cardsInfo(cards);
|
|
359
|
+
return { content: [{ type: 'text', text: JSON.stringify(response) }] };
|
|
360
|
+
});
|
|
361
|
+
const transport = new StdioServerTransport();
|
|
362
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
const BASE_URL = 'http://localhost:8765';
|
|
3
|
+
const api = axios.create({
|
|
4
|
+
baseURL: BASE_URL,
|
|
5
|
+
timeout: 5000,
|
|
6
|
+
headers: {
|
|
7
|
+
'Content-Type': 'application/json'
|
|
8
|
+
}
|
|
9
|
+
});
|
|
10
|
+
export async function callAnkiConnect(request) {
|
|
11
|
+
try {
|
|
12
|
+
const response = await api.post('/', request);
|
|
13
|
+
console.log('Request:', request);
|
|
14
|
+
console.log('===RESPONSE===');
|
|
15
|
+
console.log(response.data);
|
|
16
|
+
return response.data;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.log('Anki Connect Error:', error);
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|