plugvan-mcp 0.1.1__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.
plugvan/__init__.py ADDED
File without changes
plugvan/cli.py ADDED
@@ -0,0 +1,54 @@
1
+ """The `plugvan-mcp` console script: `plugvan-mcp <integration>` starts that integration's MCP
2
+ server over STDIO. Never reads .env; configuration comes from the environment, i.e. the env
3
+ block of the MCP client config."""
4
+
5
+ import argparse
6
+ import sys
7
+ from importlib import metadata
8
+
9
+ from plugvan import todoist
10
+
11
+ DISTRIBUTION = "plugvan-mcp"
12
+
13
+ # Integration name -> its run function, looked up at call time so tests can patch it.
14
+ INTEGRATIONS = {
15
+ "todoist": lambda: todoist.run(),
16
+ }
17
+
18
+
19
+ def _version() -> str:
20
+ try:
21
+ return metadata.version(DISTRIBUTION)
22
+ except metadata.PackageNotFoundError:
23
+ return "unknown"
24
+
25
+
26
+ def _parser() -> argparse.ArgumentParser:
27
+ parser = argparse.ArgumentParser(
28
+ prog=DISTRIBUTION,
29
+ description="Run a Plugvan MCP server over STDIO.",
30
+ )
31
+ parser.add_argument("--version", action="store_true", help="print the version and exit")
32
+ subparsers = parser.add_subparsers(dest="integration", metavar="{" + ",".join(INTEGRATIONS) + "}")
33
+ for name in INTEGRATIONS:
34
+ subparsers.add_parser(name, help=f"run the {name.capitalize()} MCP server over STDIO")
35
+ return parser
36
+
37
+
38
+ def main(argv: list[str] | None = None) -> int:
39
+ parser = _parser()
40
+ try:
41
+ args = parser.parse_args(argv)
42
+ except SystemExit as exit_: # --help (0) and usage errors (2), already printed by argparse
43
+ return exit_.code if isinstance(exit_.code, int) else 2
44
+
45
+ if args.version:
46
+ print(f"{DISTRIBUTION} {_version()}")
47
+ return 0
48
+ if args.integration is None:
49
+ parser.print_usage(sys.stderr)
50
+ print(f"{DISTRIBUTION}: error: choose an integration: {', '.join(INTEGRATIONS)}", file=sys.stderr)
51
+ return 2
52
+
53
+ INTEGRATIONS[args.integration]()
54
+ return 0
@@ -0,0 +1,22 @@
1
+ """Todoist MCP integration. run() starts the STDIO server for both entry points:
2
+ `python -m plugvan.todoist` and the `plugvan-mcp todoist` console script."""
3
+
4
+ import logging
5
+
6
+
7
+ def configure_logging() -> None:
8
+ """Logs to stderr at INFO. Calling it again is a no-op (logging.basicConfig)."""
9
+ logging.basicConfig(level=logging.INFO)
10
+
11
+
12
+ def run() -> None:
13
+ """Configures logging, then imports and runs the Todoist server over STDIO.
14
+
15
+ Logging is configured before the server module is imported: the SDK's FastMCP()
16
+ constructor calls logging.basicConfig with its own handler, and the first configuration
17
+ wins. Never reads .env; only `python -m plugvan.todoist` does that, before calling run().
18
+ """
19
+ configure_logging()
20
+ from plugvan.todoist.server import mcp
21
+
22
+ mcp.run()
@@ -0,0 +1,15 @@
1
+ """Entry point: python -m plugvan.todoist (STDIO transport).
2
+
3
+ The only place that reads .env: load_dotenv() with its defaults, so variables already set
4
+ (for example by the MCP client's env block) are not overridden. The `plugvan-mcp todoist`
5
+ console script never imports this module and never reads .env.
6
+ """
7
+
8
+ from dotenv import load_dotenv
9
+
10
+ from plugvan import todoist
11
+
12
+ if __name__ == "__main__":
13
+ todoist.configure_logging()
14
+ load_dotenv()
15
+ todoist.run()
@@ -0,0 +1,17 @@
1
+ """Todoist SDK client factory. Tools call it as client.get_api_client() so tests can patch one target."""
2
+
3
+ import logging
4
+ import os
5
+
6
+ from todoist_api_python.api import TodoistAPI
7
+
8
+ logger = logging.getLogger("todoist_mcp")
9
+
10
+
11
+ def get_api_client() -> TodoistAPI:
12
+ """Reads TODOIST_API_TOKEN from environment and returns a TodoistAPI client instance."""
13
+ token = os.getenv("TODOIST_API_TOKEN")
14
+ if not token or not token.strip():
15
+ logger.error("TODOIST_API_TOKEN is missing or empty in environment configuration", exc_info=False)
16
+ raise RuntimeError("Todoist API authentication token is not configured.")
17
+ return TodoistAPI(token.strip())
@@ -0,0 +1,25 @@
1
+ """Tool description normalization, so every supported Python publishes the same descriptions."""
2
+
3
+ from typing import Optional
4
+
5
+
6
+ def clean_docstring(doc: Optional[str]) -> Optional[str]:
7
+ """Returns doc exactly as the Python 3.13+ compiler stores a docstring.
8
+
9
+ Port of CPython's _PyCompile_CleanDoc (Python/compile.c, 3.13): tabs are expanded,
10
+ leading spaces are removed from the first line, and each later line loses up to the
11
+ common indentation of the non-blank later lines. Only spaces count as indentation, and
12
+ leading/trailing blank lines (so the trailing newline) are kept, which is where
13
+ inspect.cleandoc differs. On 3.13+ __doc__ is already clean and this is the identity.
14
+ """
15
+ if doc is None:
16
+ return None
17
+ lines = doc.expandtabs().split("\n")
18
+ first, rest = lines[0], lines[1:]
19
+ indents = [_indent(line) for line in rest if line.strip(" ")]
20
+ margin = min(indents, default=0)
21
+ return "\n".join([first.lstrip(" ")] + [line[min(margin, _indent(line)):] for line in rest])
22
+
23
+
24
+ def _indent(line: str) -> int:
25
+ return len(line) - len(line.lstrip(" "))
@@ -0,0 +1,32 @@
1
+ """Output language selection: PLUGVAN_LANG, read from the environment on every call."""
2
+
3
+ import logging
4
+ import os
5
+
6
+ logger = logging.getLogger("todoist_mcp")
7
+
8
+ SUPPORTED_LANGUAGES = ("en", "tr")
9
+ DEFAULT_LANGUAGE = "en"
10
+ MAX_LOGGED_LANGUAGE_VALUE_LENGTH = 32
11
+
12
+ _unsupported_value_warned = False
13
+
14
+
15
+ def current_language() -> str:
16
+ """Returns the selected output language: PLUGVAN_LANG trimmed and lowercased if it is a
17
+ supported code, otherwise the default. An unsupported value is logged once per process."""
18
+ global _unsupported_value_warned
19
+ raw = os.getenv("PLUGVAN_LANG")
20
+ if raw is None or not raw.strip():
21
+ return DEFAULT_LANGUAGE
22
+ language = raw.strip().lower()
23
+ if language in SUPPORTED_LANGUAGES:
24
+ return language
25
+ if not _unsupported_value_warned:
26
+ _unsupported_value_warned = True
27
+ logger.warning(
28
+ "Unsupported PLUGVAN_LANG value %r; using %r",
29
+ raw[:MAX_LOGGED_LANGUAGE_VALUE_LENGTH],
30
+ DEFAULT_LANGUAGE,
31
+ )
32
+ return DEFAULT_LANGUAGE
@@ -0,0 +1,12 @@
1
+ """Input length limits for Todoist MCP tool arguments."""
2
+
3
+ MAX_CONTENT_LENGTH = 500
4
+ MAX_DESCRIPTION_LENGTH = 4096
5
+ MAX_PROJECT_NAME_LENGTH = 120
6
+ MAX_DUE_STRING_LENGTH = 150
7
+ MAX_FILTER_QUERY_LENGTH = 500
8
+ MAX_TASK_ID_LENGTH = 100
9
+ MAX_COLOR_LENGTH = 50
10
+ MAX_LABEL_NAME_LENGTH = 60
11
+ MAX_SECTION_NAME_LENGTH = 120
12
+ MAX_COMMENT_LENGTH = 4096
@@ -0,0 +1,427 @@
1
+ """User-facing tool messages: one catalog per output language, looked up by key.
2
+
3
+ Templates use str.format named placeholders. A key that is missing in the selected language
4
+ raises MissingMessageError; tools re-raise it before their generic `except Exception` handler,
5
+ so a missing translation is never turned into a generic failure message.
6
+ """
7
+
8
+ from plugvan.todoist.language import current_language
9
+
10
+
11
+ class MissingMessageError(Exception):
12
+ """A message key has no entry in the selected language's catalog."""
13
+
14
+
15
+ CATALOGS: dict[str, dict[str, str]] = {
16
+ "en": {
17
+ # Shared: name resolution
18
+ "ambiguous.project": "⚠️ Multiple projects found for '{query}'; please use the project ID:",
19
+ "ambiguous.section": "⚠️ Multiple sections found for '{query}'; please use the section ID:",
20
+ "ambiguous.label": "⚠️ Multiple labels found for '{query}'; please use the label ID:",
21
+ "inbox.display_name": "Inbox",
22
+ "inbox.marker": " [Inbox]",
23
+ "project.not_found": "⚠️ Project not found: '{name}'.",
24
+ "section.not_found": "⚠️ Section not found: '{name}'.",
25
+ # Shared: input validation
26
+ "invalid.task_id_empty": "❌ Invalid input: Task ID cannot be empty or whitespace.",
27
+ "invalid.task_id_too_long": "❌ Invalid input: Task ID exceeds maximum length of {limit} characters.",
28
+ "invalid.task_content_empty": "❌ Invalid input: Task content cannot be empty or whitespace.",
29
+ "invalid.task_content_too_long": "❌ Invalid input: Task content exceeds maximum length of {limit} characters.",
30
+ "invalid.task_description_too_long": "❌ Invalid input: Task description exceeds maximum length of {limit} characters.",
31
+ "invalid.project_name_too_long": "❌ Invalid input: Project name exceeds maximum length of {limit} characters.",
32
+ "invalid.due_string_too_long": "❌ Invalid input: Due date string exceeds maximum length of {limit} characters.",
33
+ "invalid.priority": "❌ Invalid input: Priority must be an integer between 1 (Normal) and 4 (Urgent).",
34
+ "invalid.labels_not_list": "❌ Invalid input: Labels must be a list of strings.",
35
+ "invalid.section_identifier_too_long": "❌ Invalid input: Section identifier exceeds maximum length of {limit} characters.",
36
+ "invalid.parent_task_id_too_long": "❌ Invalid input: Parent task ID exceeds maximum length of {limit} characters.",
37
+ "invalid.deadline_date_format": "❌ Invalid input: deadline_date must be in YYYY-MM-DD format (received: '{value}').",
38
+ "invalid.filter_query_empty": "❌ Invalid input: Filter query cannot be empty or whitespace.",
39
+ "invalid.filter_query_too_long": "❌ Invalid input: Filter query exceeds maximum length of {limit} characters.",
40
+ # Tasks
41
+ "task.deadline_plan_restricted": "❌ The deadline feature is not available on the Todoist Free/Beginner plan — a Pro or Business plan is required (403 Forbidden).",
42
+ "task.create_failed": "❌ Todoist task creation failed. Check server logs for details.",
43
+ "task.due_not_specified": "Not specified",
44
+ "task.created.section_line": "\n• Section: {name} (ID: {id})",
45
+ "task.created.labels_line": "\n• Labels: {labels}",
46
+ "task.created.deadline_line": "\n• Deadline: {date}",
47
+ "task.created": (
48
+ "✅ Task created successfully!\n"
49
+ "• ID: {id}\n"
50
+ "• Title: {content}\n"
51
+ "• Project: {project}"
52
+ "{section_line}\n"
53
+ "• Priority: p{priority}\n"
54
+ "• Due / Recurrence: {due}"
55
+ "{deadline_line}"
56
+ "{labels_line}\n"
57
+ "• URL: {url}"
58
+ ),
59
+ "task.list.empty": "ℹ️ No open tasks match the filter '{query}'.",
60
+ "task.priority.4": "🔴 p4 (Urgent)",
61
+ "task.priority.3": "🟠 p3 (High)",
62
+ "task.priority.2": "🔵 p2 (Medium)",
63
+ "task.priority.1": "⚪ p1 (Normal)",
64
+ "task.list.header": "📋 Open Tasks (Filter: '{query}', Total: {count}):",
65
+ "task.list.no_due": "No due date",
66
+ "task.list.priority_line": " • Priority: {priority}",
67
+ "task.list.due_line": " • Due: {due}",
68
+ "task.list.description_line": " • Description: {description}",
69
+ "task.list_failed": "❌ Failed to retrieve tasks from Todoist. Check server logs for details.",
70
+ "task.detail.not_set": "Not set",
71
+ "task.detail.none": "None",
72
+ "task.detail.no_parent": "None (top-level task)",
73
+ "task.detail": (
74
+ "📄 Task Details (ID: {id})\n"
75
+ "• Title: {content}\n"
76
+ "• Description: {description}\n"
77
+ "• Project ID: {project_id}\n"
78
+ "• Section ID: {section}\n"
79
+ "• Parent Task ID (parent_id): {parent}\n"
80
+ "• Priority: p{priority}\n"
81
+ "• Due / Recurrence: {due}\n"
82
+ "• Labels: {labels}\n"
83
+ "• URL: {url}"
84
+ ),
85
+ "task.get_failed": "❌ Failed to retrieve task from Todoist (ID: {id}). Check server logs for details.",
86
+ "task.completed": "✅ Task completed successfully (ID: {id}).",
87
+ "task.complete_unconfirmed": "⚠️ The task could not be completed, or it may already be completed (ID: {id}).",
88
+ "task.complete_failed": "❌ Failed to complete task in Todoist (ID: {id}). Check server logs for details.",
89
+ "task.reopened": "🔄 Task reopened successfully (ID: {id}).",
90
+ "task.reopen_unconfirmed": "⚠️ The task could not be reopened, or it may already be open (ID: {id}).",
91
+ "task.reopen_failed": "❌ Failed to reopen task in Todoist (ID: {id}). Check server logs for details.",
92
+ "task.update.no_fields": "⚠️ No fields to update were specified. Please provide at least one parameter (content, description, project_name, due_string, priority, labels, section_name_or_id, parent_id, deadline_date).",
93
+ "task.update_failed": "❌ Failed to update task in Todoist (ID: {id}). Check server logs for details.",
94
+ "task.update.target_project_not_found": "⚠️ Target project not found: '{name}'.",
95
+ "task.update.target_section_not_found": "⚠️ Target section not found: '{name}'.",
96
+ "task.update.change.content": "Title: '{value}'",
97
+ "task.update.change.description": "Description: '{value}'",
98
+ "task.update.change.due": "Due: '{value}'",
99
+ "task.update.change.priority": "Priority: p{value}",
100
+ "task.update.change.labels": "Labels: {value}",
101
+ "task.update.change.labels_cleared": "Cleared",
102
+ "task.update.change.deadline": "Deadline: '{value}'",
103
+ "task.update.change.project": "Target Project: '{name}' (ID: {id})",
104
+ "task.update.change.section": "Target Section: '{name}' (ID: {id})",
105
+ "task.update.change.parent": "Parent Task (parent_id): '{id}'",
106
+ "task.update.change.parent_removed": "Parent Task: Removed (task moved to the top level)",
107
+ "task.updated": "✅ Task updated successfully (ID: {id})!\nChanges made:\n{changes}",
108
+ "task.deleted": "🗑️ Task permanently deleted (ID: {id}).",
109
+ "task.delete_unconfirmed": "⚠️ The task could not be deleted (ID: {id}). It may already be deleted or not exist.",
110
+ "task.delete_failed": "❌ Failed to delete task in Todoist (ID: {id}). Check server logs for details.",
111
+ # Shared: target display in delete messages
112
+ "display.named": "'{name}' (ID: {id})",
113
+ "display.id_only": "ID: {id}",
114
+ # Shared: input validation (projects)
115
+ "invalid.project_id_empty": "❌ Invalid input: Project ID cannot be empty or whitespace.",
116
+ "invalid.project_id_too_long": "❌ Invalid input: Project ID exceeds maximum length of {limit} characters.",
117
+ "invalid.project_name_empty": "❌ Invalid input: Project name cannot be empty or whitespace.",
118
+ "invalid.parent_project_identifier_too_long": "❌ Invalid input: Parent project identifier exceeds maximum length of {limit} characters.",
119
+ "invalid.color_too_long": "❌ Invalid input: Color name exceeds maximum length of {limit} characters.",
120
+ "invalid.project_identifier_empty": "❌ Invalid input: Project identifier cannot be empty or whitespace.",
121
+ "invalid.project_identifier_too_long": "❌ Invalid input: Project identifier exceeds maximum length of {limit} characters.",
122
+ "invalid.new_project_name_empty": "❌ Invalid input: New project name cannot be empty or whitespace.",
123
+ "invalid.new_project_name_too_long": "❌ Invalid input: New project name exceeds maximum length of {limit} characters.",
124
+ # Projects
125
+ "project.list.empty": "ℹ️ No projects found in your Todoist account.",
126
+ "project.list.header": "📁 Available Projects (Total: {count}):",
127
+ "project.list.color_suffix": ", Color: {color}",
128
+ "project.list.url_line": "{indent}• URL: {url}",
129
+ "project.list_failed": "❌ Failed to list projects from Todoist. Check server logs for details.",
130
+ "project.detail.none": "None",
131
+ "project.detail.no_parent": "None (top-level project)",
132
+ "project.detail.yes": "Yes",
133
+ "project.detail.no": "No",
134
+ "project.detail.not_set": "Not set",
135
+ "project.detail": (
136
+ "📁 Project Details (ID: {id})\n"
137
+ "• Name: {name}\n"
138
+ "• Color: {color}\n"
139
+ "• Parent Project ID (parent_id): {parent}\n"
140
+ "• Favorite: {favorite}\n"
141
+ "• View Style: {view_style}\n"
142
+ "• URL: {url}"
143
+ ),
144
+ "project.get_failed": "❌ Failed to retrieve project from Todoist (ID: {id}). Check server logs for details.",
145
+ "project.create_failed": "❌ Failed to create project '{name}' in Todoist. Check server logs for details.",
146
+ "project.parent_not_found": "⚠️ Parent project not found: '{name}'.",
147
+ "project.created.parent_line": "\n• Parent Project: '{name}' (ID: {id})",
148
+ "project.created.color_line": "\n• Color: {color}",
149
+ "project.created.url_line": "\n• URL: {url}",
150
+ "project.created": "✅ Project created successfully!\n• ID: {id}\n• Name: {name}{parent_line}{color_line}{url_line}",
151
+ "project.update.no_fields": "⚠️ No fields to update were specified. Please provide the name or color parameter.",
152
+ "project.update_failed": "❌ Failed to update project '{name}' in Todoist. Check server logs for details.",
153
+ "project.update.not_found": "⚠️ Project to update not found: '{name}'.",
154
+ "project.update.change.name": "Name: '{value}'",
155
+ "project.update.change.color": "Color: '{value}'",
156
+ "project.updated": "✅ Project updated successfully (ID: {id})!\nChanges made:\n{changes}",
157
+ "project.delete_failed": "❌ Failed to delete project '{name}' in Todoist. Check server logs for details.",
158
+ "project.delete.not_found": "⚠️ Project to delete not found: '{name}'.",
159
+ "project.deleted": "🗑️ Project deleted successfully: {target}.",
160
+ "project.delete_unconfirmed": "⚠️ The project could not be deleted: {target}.",
161
+ # Sections
162
+ "invalid.section_name_empty": "❌ Invalid input: Section name cannot be empty or whitespace.",
163
+ "invalid.section_name_too_long": "❌ Invalid input: Section name exceeds maximum length of {limit} characters.",
164
+ "invalid.section_identifier_empty": "❌ Invalid input: Section identifier cannot be empty or whitespace.",
165
+ "invalid.new_section_name_empty": "❌ Invalid input: New section name cannot be empty or whitespace.",
166
+ "invalid.new_section_name_too_long": "❌ Invalid input: New section name exceeds maximum length of {limit} characters.",
167
+ "section.create_failed": "❌ Failed to create section '{name}' in Todoist. Check server logs for details.",
168
+ "section.create.project_not_found": "⚠️ Target project for the new section not found: '{name}'.",
169
+ "section.created": "✅ Section created successfully!\n• ID: {id}\n• Name: {name}\n• Project: '{project}' (ID: {project_id})",
170
+ "section.list_failed": "❌ Failed to list sections from Todoist. Check server logs for details.",
171
+ "section.list.empty_in_project": "ℹ️ No sections found in the Todoist project '{project}'.",
172
+ "section.list.empty_all": "ℹ️ No sections found in your Todoist account.",
173
+ "section.list.header_project": "📑 Available Sections (Project '{project}', Total: {count}):",
174
+ "section.list.header_all": "📑 Available Sections (All Projects, Total: {count}):",
175
+ "section.list.line": "{index}. [{id}] {name} (Project ID: {project_id})",
176
+ "section.update_failed": "❌ Failed to update section '{name}' in Todoist. Check server logs for details.",
177
+ "section.update.not_found": "⚠️ Section to update not found: '{name}'.",
178
+ "section.updated": "✅ Section updated successfully (ID: {id})!\n• Old name: '{old_name}'\n• New name: '{new_name}'",
179
+ "section.delete_failed": "❌ Failed to delete section '{name}' in Todoist. Check server logs for details.",
180
+ "section.delete.not_found": "⚠️ Section to delete not found: '{name}'.",
181
+ "section.deleted": "🗑️ Section deleted successfully: {target}.",
182
+ "section.delete_unconfirmed": "⚠️ The section could not be deleted: {target}.",
183
+ # Labels
184
+ "label.list.empty": "ℹ️ No labels found in your Todoist account.",
185
+ "label.list.header": "🏷️ Available Labels (Total: {count}):",
186
+ "label.list.color_suffix": ", Color: {color}",
187
+ "label.list_failed": "❌ Failed to list labels from Todoist. Check server logs for details.",
188
+ "invalid.label_name_empty": "❌ Invalid input: Label name cannot be empty or whitespace.",
189
+ "invalid.label_name_too_long": "❌ Invalid input: Label name exceeds maximum length of {limit} characters.",
190
+ "label.created.color_line": "\n• Color: {color}",
191
+ "label.created": "✅ Label created successfully!\n• ID: {id}\n• Name: @{name}{color_line}",
192
+ "label.create_failed": "❌ Failed to create label '@{name}' in Todoist. Check server logs for details.",
193
+ "invalid.label_identifier_empty": "❌ Invalid input: Label identifier cannot be empty or whitespace.",
194
+ "invalid.label_identifier_too_long": "❌ Invalid input: Label identifier exceeds maximum length of {limit} characters.",
195
+ "invalid.new_label_name_too_long": "❌ Invalid input: New label name exceeds maximum length of {limit} characters.",
196
+ "label.update.no_fields": "⚠️ No fields to update were specified. Please provide the new_name or color parameter.",
197
+ "label.update_failed": "❌ Failed to update label '@{name}' in Todoist. Check server logs for details.",
198
+ "label.update.not_found": "⚠️ Label to update not found: '@{name}'.",
199
+ "label.update.change.name": "Name: '@{value}'",
200
+ "label.update.change.color": "Color: '{value}'",
201
+ "label.updated": "✅ Label updated successfully (ID: {id})!\nChanges made:\n{changes}",
202
+ "label.delete_failed": "❌ Failed to delete label '@{name}' in Todoist. Check server logs for details.",
203
+ "label.delete.not_found": "⚠️ Label to delete not found: '@{name}'.",
204
+ "label.display.named": "'@{name}' (ID: {id})",
205
+ "label.deleted": "🗑️ Label deleted successfully: {target}.",
206
+ "label.delete_unconfirmed": "⚠️ The label could not be deleted: {target}.",
207
+ # Comments
208
+ "invalid.comment_content_empty": "❌ Invalid input: Comment content cannot be empty or whitespace.",
209
+ "invalid.comment_content_too_long": "❌ Invalid input: Comment content exceeds maximum length of {limit} characters.",
210
+ "comment.added.posted_line": "\n• Date: {posted_at}",
211
+ "comment.added": "💬 Comment added successfully!\n• Comment ID: {id}\n• Task ID: {task_id}\n• Content: {content}{posted_line}",
212
+ "comment.add_failed": "❌ Failed to add comment to task (ID: {task_id}) in Todoist. Check server logs for details.",
213
+ "comment.list.empty": "ℹ️ No comments or notes found for the task (ID: {task_id}).",
214
+ "comment.list.header": "💬 Task Comments (Task ID: {task_id}, Total: {count}):",
215
+ "comment.list_failed": "❌ Failed to retrieve comments for task (ID: {task_id}) from Todoist. Check server logs for details.",
216
+ },
217
+ "tr": {
218
+ # Shared: name resolution
219
+ "ambiguous.project": "⚠️ '{query}' için birden fazla proje bulundu; lütfen proje ID'sini kullanın:",
220
+ "ambiguous.section": "⚠️ '{query}' için birden fazla bölüm bulundu; lütfen bölüm ID'sini kullanın:",
221
+ "ambiguous.label": "⚠️ '{query}' için birden fazla etiket bulundu; lütfen etiket ID'sini kullanın:",
222
+ "inbox.display_name": "Gelen Kutusu",
223
+ "inbox.marker": " [Gelen Kutusu]",
224
+ "project.not_found": "⚠️ Belirtilen proje bulunamadı: '{name}'.",
225
+ "section.not_found": "⚠️ Belirtilen bölüm bulunamadı: '{name}'.",
226
+ # Shared: input validation
227
+ "invalid.task_id_empty": "❌ Invalid input: Task ID cannot be empty or whitespace.",
228
+ "invalid.task_id_too_long": "❌ Invalid input: Task ID exceeds maximum length of {limit} characters.",
229
+ "invalid.task_content_empty": "❌ Invalid input: Task content cannot be empty or whitespace.",
230
+ "invalid.task_content_too_long": "❌ Invalid input: Task content exceeds maximum length of {limit} characters.",
231
+ "invalid.task_description_too_long": "❌ Invalid input: Task description exceeds maximum length of {limit} characters.",
232
+ "invalid.project_name_too_long": "❌ Invalid input: Project name exceeds maximum length of {limit} characters.",
233
+ "invalid.due_string_too_long": "❌ Invalid input: Due date string exceeds maximum length of {limit} characters.",
234
+ "invalid.priority": "❌ Invalid input: Priority must be an integer between 1 (Normal) and 4 (Urgent).",
235
+ "invalid.labels_not_list": "❌ Invalid input: Labels must be a list of strings.",
236
+ "invalid.section_identifier_too_long": "❌ Invalid input: Section identifier exceeds maximum length of {limit} characters.",
237
+ "invalid.parent_task_id_too_long": "❌ Invalid input: Parent task ID exceeds maximum length of {limit} characters.",
238
+ "invalid.deadline_date_format": "❌ Invalid input: deadline_date must be in YYYY-MM-DD format (received: '{value}').",
239
+ "invalid.filter_query_empty": "❌ Invalid input: Filter query cannot be empty or whitespace.",
240
+ "invalid.filter_query_too_long": "❌ Invalid input: Filter query exceeds maximum length of {limit} characters.",
241
+ # Tasks
242
+ "task.deadline_plan_restricted": "❌ Deadline özelliği Todoist Free/Beginner planında desteklenmiyor — Pro veya Business plan gerekiyor (403 Forbidden).",
243
+ "task.create_failed": "❌ Todoist task creation failed. Check server logs for details.",
244
+ "task.due_not_specified": "Belirtilmedi",
245
+ "task.created.section_line": "\n• Bölüm: {name} (ID: {id})",
246
+ "task.created.labels_line": "\n• Etiketler: {labels}",
247
+ "task.created.deadline_line": "\n• Son Teslim Tarihi (Deadline): {date}",
248
+ "task.created": (
249
+ "✅ Görev başarıyla oluşturuldu!\n"
250
+ "• ID: {id}\n"
251
+ "• Başlık: {content}\n"
252
+ "• Proje: {project}"
253
+ "{section_line}\n"
254
+ "• Öncelik: p{priority}\n"
255
+ "• Tarih / Tekrar: {due}"
256
+ "{deadline_line}"
257
+ "{labels_line}\n"
258
+ "• URL: {url}"
259
+ ),
260
+ "task.list.empty": "ℹ️ '{query}' filtresine uygun açık görev bulunamadı.",
261
+ "task.priority.4": "🔴 p4 (Çok Acil)",
262
+ "task.priority.3": "🟠 p3 (Yüksek)",
263
+ "task.priority.2": "🔵 p2 (Orta)",
264
+ "task.priority.1": "⚪ p1 (Normal)",
265
+ "task.list.header": "📋 Açık Görevler (Filtre: '{query}', Toplam: {count}):",
266
+ "task.list.no_due": "Tarih yok",
267
+ "task.list.priority_line": " • Öncelik: {priority}",
268
+ "task.list.due_line": " • Tarih: {due}",
269
+ "task.list.description_line": " • Açıklama: {description}",
270
+ "task.list_failed": "❌ Failed to retrieve tasks from Todoist. Check server logs for details.",
271
+ "task.detail.not_set": "Belirlenmedi",
272
+ "task.detail.none": "Yok",
273
+ "task.detail.no_parent": "Yok (üst seviye görev)",
274
+ "task.detail": (
275
+ "📄 Görev Detayları (ID: {id})\n"
276
+ "• Başlık: {content}\n"
277
+ "• Açıklama: {description}\n"
278
+ "• Proje ID: {project_id}\n"
279
+ "• Bölüm ID: {section}\n"
280
+ "• Üst Görev ID (parent_id): {parent}\n"
281
+ "• Öncelik: p{priority}\n"
282
+ "• Tarih / Tekrar: {due}\n"
283
+ "• Etiketler: {labels}\n"
284
+ "• URL: {url}"
285
+ ),
286
+ "task.get_failed": "❌ Failed to retrieve task from Todoist (ID: {id}). Check server logs for details.",
287
+ "task.completed": "✅ Görev başarıyla tamamlandı (ID: {id}).",
288
+ "task.complete_unconfirmed": "⚠️ Görev tamamlanamadı veya zaten tamamlanmış olabilir (ID: {id}).",
289
+ "task.complete_failed": "❌ Failed to complete task in Todoist (ID: {id}). Check server logs for details.",
290
+ "task.reopened": "🔄 Görev başarıyla yeniden açıldı (ID: {id}).",
291
+ "task.reopen_unconfirmed": "⚠️ Görev yeniden açılamadı veya zaten açık olabilir (ID: {id}).",
292
+ "task.reopen_failed": "❌ Failed to reopen task in Todoist (ID: {id}). Check server logs for details.",
293
+ "task.update.no_fields": "⚠️ Güncellenecek hiçbir alan belirtilmedi. Lütfen en az bir parametre (content, description, project_name, due_string, priority, labels, section_name_or_id, parent_id, deadline_date) girin.",
294
+ "task.update_failed": "❌ Failed to update task in Todoist (ID: {id}). Check server logs for details.",
295
+ "task.update.target_project_not_found": "⚠️ Hedef proje bulunamadı: '{name}'.",
296
+ "task.update.target_section_not_found": "⚠️ Hedef bölüm bulunamadı: '{name}'.",
297
+ "task.update.change.content": "Başlık: '{value}'",
298
+ "task.update.change.description": "Açıklama: '{value}'",
299
+ "task.update.change.due": "Tarih: '{value}'",
300
+ "task.update.change.priority": "Öncelik: p{value}",
301
+ "task.update.change.labels": "Etiketler: {value}",
302
+ "task.update.change.labels_cleared": "Temizlendi",
303
+ "task.update.change.deadline": "Son Teslim Tarihi (Deadline): '{value}'",
304
+ "task.update.change.project": "Hedef Proje: '{name}' (ID: {id})",
305
+ "task.update.change.section": "Hedef Bölüm: '{name}' (ID: {id})",
306
+ "task.update.change.parent": "Üst Görev (parent_id): '{id}'",
307
+ "task.update.change.parent_removed": "Üst Görev: Kaldırıldı (görev üst seviyeye taşındı)",
308
+ "task.updated": "✅ Görev başarıyla güncellendi (ID: {id})!\nYapılan değişiklikler:\n{changes}",
309
+ "task.deleted": "🗑️ Görev kalıcı olarak silindi (ID: {id}).",
310
+ "task.delete_unconfirmed": "⚠️ Görev silinemedi (ID: {id}). Görev zaten silinmiş veya bulunamıyor olabilir.",
311
+ "task.delete_failed": "❌ Failed to delete task in Todoist (ID: {id}). Check server logs for details.",
312
+ # Shared: target display in delete messages
313
+ "display.named": "'{name}' (ID: {id})",
314
+ "display.id_only": "ID: {id}",
315
+ # Shared: input validation (projects)
316
+ "invalid.project_id_empty": "❌ Invalid input: Project ID cannot be empty or whitespace.",
317
+ "invalid.project_id_too_long": "❌ Invalid input: Project ID exceeds maximum length of {limit} characters.",
318
+ "invalid.project_name_empty": "❌ Invalid input: Project name cannot be empty or whitespace.",
319
+ "invalid.parent_project_identifier_too_long": "❌ Invalid input: Parent project identifier exceeds maximum length of {limit} characters.",
320
+ "invalid.color_too_long": "❌ Invalid input: Color name exceeds maximum length of {limit} characters.",
321
+ "invalid.project_identifier_empty": "❌ Invalid input: Project identifier cannot be empty or whitespace.",
322
+ "invalid.project_identifier_too_long": "❌ Invalid input: Project identifier exceeds maximum length of {limit} characters.",
323
+ "invalid.new_project_name_empty": "❌ Invalid input: New project name cannot be empty or whitespace.",
324
+ "invalid.new_project_name_too_long": "❌ Invalid input: New project name exceeds maximum length of {limit} characters.",
325
+ # Projects
326
+ "project.list.empty": "ℹ️ Todoist hesabınızda herhangi bir proje bulunamadı.",
327
+ "project.list.header": "📁 Mevcut Projeler (Toplam: {count}):",
328
+ "project.list.color_suffix": ", Renk: {color}",
329
+ "project.list.url_line": "{indent}• URL: {url}",
330
+ "project.list_failed": "❌ Failed to list projects from Todoist. Check server logs for details.",
331
+ "project.detail.none": "Yok",
332
+ "project.detail.no_parent": "Yok (üst seviye proje)",
333
+ "project.detail.yes": "Evet",
334
+ "project.detail.no": "Hayır",
335
+ "project.detail.not_set": "Belirlenmedi",
336
+ "project.detail": (
337
+ "📁 Proje Detayları (ID: {id})\n"
338
+ "• İsim: {name}\n"
339
+ "• Renk: {color}\n"
340
+ "• Üst Proje ID (parent_id): {parent}\n"
341
+ "• Favori mi: {favorite}\n"
342
+ "• Görünüm Stili: {view_style}\n"
343
+ "• URL: {url}"
344
+ ),
345
+ "project.get_failed": "❌ Failed to retrieve project from Todoist (ID: {id}). Check server logs for details.",
346
+ "project.create_failed": "❌ Failed to create project '{name}' in Todoist. Check server logs for details.",
347
+ "project.parent_not_found": "⚠️ Belirtilen üst proje bulunamadı: '{name}'.",
348
+ "project.created.parent_line": "\n• Üst Proje: '{name}' (ID: {id})",
349
+ "project.created.color_line": "\n• Renk: {color}",
350
+ "project.created.url_line": "\n• URL: {url}",
351
+ "project.created": "✅ Proje başarıyla oluşturuldu!\n• ID: {id}\n• İsim: {name}{parent_line}{color_line}{url_line}",
352
+ "project.update.no_fields": "⚠️ Güncellenecek hiçbir alan belirtilmedi. Lütfen name veya color parametresi girin.",
353
+ "project.update_failed": "❌ Failed to update project '{name}' in Todoist. Check server logs for details.",
354
+ "project.update.not_found": "⚠️ Güncellenecek proje bulunamadı: '{name}'.",
355
+ "project.update.change.name": "İsim: '{value}'",
356
+ "project.update.change.color": "Renk: '{value}'",
357
+ "project.updated": "✅ Proje başarıyla güncellendi (ID: {id})!\nYapılan değişiklikler:\n{changes}",
358
+ "project.delete_failed": "❌ Failed to delete project '{name}' in Todoist. Check server logs for details.",
359
+ "project.delete.not_found": "⚠️ Silinecek proje bulunamadı: '{name}'.",
360
+ "project.deleted": "🗑️ Proje başarıyla silindi: {target}.",
361
+ "project.delete_unconfirmed": "⚠️ Proje silinemedi: {target}.",
362
+ # Sections
363
+ "invalid.section_name_empty": "❌ Invalid input: Section name cannot be empty or whitespace.",
364
+ "invalid.section_name_too_long": "❌ Invalid input: Section name exceeds maximum length of {limit} characters.",
365
+ "invalid.section_identifier_empty": "❌ Invalid input: Section identifier cannot be empty or whitespace.",
366
+ "invalid.new_section_name_empty": "❌ Invalid input: New section name cannot be empty or whitespace.",
367
+ "invalid.new_section_name_too_long": "❌ Invalid input: New section name exceeds maximum length of {limit} characters.",
368
+ "section.create_failed": "❌ Failed to create section '{name}' in Todoist. Check server logs for details.",
369
+ "section.create.project_not_found": "⚠️ Bölüm oluşturulacak hedef proje bulunamadı: '{name}'.",
370
+ "section.created": "✅ Bölüm başarıyla oluşturuldu!\n• ID: {id}\n• İsim: {name}\n• Proje: '{project}' (ID: {project_id})",
371
+ "section.list_failed": "❌ Failed to list sections from Todoist. Check server logs for details.",
372
+ "section.list.empty_in_project": "ℹ️ Todoist '{project}' projesinde herhangi bir bölüm bulunamadı.",
373
+ "section.list.empty_all": "ℹ️ Todoist hesabınızda herhangi bir bölüm bulunamadı.",
374
+ "section.list.header_project": "📑 Mevcut Bölümler ('{project}' Projesi, Toplam: {count}):",
375
+ "section.list.header_all": "📑 Mevcut Bölümler (Tüm Projeler, Toplam: {count}):",
376
+ "section.list.line": "{index}. [{id}] {name} (Proje ID: {project_id})",
377
+ "section.update_failed": "❌ Failed to update section '{name}' in Todoist. Check server logs for details.",
378
+ "section.update.not_found": "⚠️ Güncellenecek bölüm bulunamadı: '{name}'.",
379
+ "section.updated": "✅ Bölüm başarıyla güncellendi (ID: {id})!\n• Eski isim: '{old_name}'\n• Yeni isim: '{new_name}'",
380
+ "section.delete_failed": "❌ Failed to delete section '{name}' in Todoist. Check server logs for details.",
381
+ "section.delete.not_found": "⚠️ Silinecek bölüm bulunamadı: '{name}'.",
382
+ "section.deleted": "🗑️ Bölüm başarıyla silindi: {target}.",
383
+ "section.delete_unconfirmed": "⚠️ Bölüm silinemedi: {target}.",
384
+ # Labels
385
+ "label.list.empty": "ℹ️ Todoist hesabınızda herhangi bir etiket bulunamadı.",
386
+ "label.list.header": "🏷️ Mevcut Etiketler (Toplam: {count}):",
387
+ "label.list.color_suffix": ", Renk: {color}",
388
+ "label.list_failed": "❌ Failed to list labels from Todoist. Check server logs for details.",
389
+ "invalid.label_name_empty": "❌ Invalid input: Label name cannot be empty or whitespace.",
390
+ "invalid.label_name_too_long": "❌ Invalid input: Label name exceeds maximum length of {limit} characters.",
391
+ "label.created.color_line": "\n• Renk: {color}",
392
+ "label.created": "✅ Etiket başarıyla oluşturuldu!\n• ID: {id}\n• İsim: @{name}{color_line}",
393
+ "label.create_failed": "❌ Failed to create label '@{name}' in Todoist. Check server logs for details.",
394
+ "invalid.label_identifier_empty": "❌ Invalid input: Label identifier cannot be empty or whitespace.",
395
+ "invalid.label_identifier_too_long": "❌ Invalid input: Label identifier exceeds maximum length of {limit} characters.",
396
+ "invalid.new_label_name_too_long": "❌ Invalid input: New label name exceeds maximum length of {limit} characters.",
397
+ "label.update.no_fields": "⚠️ Güncellenecek hiçbir alan belirtilmedi. Lütfen new_name veya color parametresi girin.",
398
+ "label.update_failed": "❌ Failed to update label '@{name}' in Todoist. Check server logs for details.",
399
+ "label.update.not_found": "⚠️ Güncellenecek etiket bulunamadı: '@{name}'.",
400
+ "label.update.change.name": "İsim: '@{value}'",
401
+ "label.update.change.color": "Renk: '{value}'",
402
+ "label.updated": "✅ Etiket başarıyla güncellendi (ID: {id})!\nYapılan değişiklikler:\n{changes}",
403
+ "label.delete_failed": "❌ Failed to delete label '@{name}' in Todoist. Check server logs for details.",
404
+ "label.delete.not_found": "⚠️ Silinecek etiket bulunamadı: '@{name}'.",
405
+ "label.display.named": "'@{name}' (ID: {id})",
406
+ "label.deleted": "🗑️ Etiket başarıyla silindi: {target}.",
407
+ "label.delete_unconfirmed": "⚠️ Etiket silinemedi: {target}.",
408
+ # Comments
409
+ "invalid.comment_content_empty": "❌ Invalid input: Comment content cannot be empty or whitespace.",
410
+ "invalid.comment_content_too_long": "❌ Invalid input: Comment content exceeds maximum length of {limit} characters.",
411
+ "comment.added.posted_line": "\n• Tarih: {posted_at}",
412
+ "comment.added": "💬 Yorum başarıyla eklendi!\n• Yorum ID: {id}\n• Görev ID: {task_id}\n• İçerik: {content}{posted_line}",
413
+ "comment.add_failed": "❌ Failed to add comment to task (ID: {task_id}) in Todoist. Check server logs for details.",
414
+ "comment.list.empty": "ℹ️ Göreve ait (ID: {task_id}) herhangi bir yorum veya not bulunamadı.",
415
+ "comment.list.header": "💬 Görev Yorumları (Görev ID: {task_id}, Toplam: {count}):",
416
+ "comment.list_failed": "❌ Failed to retrieve comments for task (ID: {task_id}) from Todoist. Check server logs for details.",
417
+ },
418
+ }
419
+
420
+
421
+ def message(key: str, **params) -> str:
422
+ """Returns the message `key` in the selected output language, formatted with `params`."""
423
+ language = current_language()
424
+ template = CATALOGS.get(language, {}).get(key)
425
+ if template is None:
426
+ raise MissingMessageError(f"No '{key}' message for language '{language}'")
427
+ return template.format(**params)