@imenam/mcp-organizer 0.0.0-stage → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +548 -3
  2. package/dist/shared/logic/first-account.d.ts +13 -0
  3. package/dist/shared/logic/first-account.js +15 -0
  4. package/dist/shared/logic/labels.d.ts +5 -0
  5. package/dist/shared/logic/labels.js +17 -0
  6. package/dist/shared/logic/positions.d.ts +9 -0
  7. package/dist/shared/logic/positions.js +19 -0
  8. package/dist/shared/logic/spaces.d.ts +13 -0
  9. package/dist/shared/logic/spaces.js +48 -0
  10. package/dist/shared/logic/test-summary.d.ts +28 -0
  11. package/dist/shared/logic/test-summary.js +35 -0
  12. package/dist/shared/schemas/agent-api.schema.d.ts +269 -0
  13. package/dist/shared/schemas/agent-api.schema.js +97 -0
  14. package/dist/shared/schemas/agent-tools.schema.d.ts +818 -0
  15. package/dist/shared/schemas/agent-tools.schema.js +198 -0
  16. package/dist/shared/schemas/api.schema.d.ts +249 -0
  17. package/dist/shared/schemas/api.schema.js +116 -0
  18. package/dist/shared/schemas/entities.schema.d.ts +472 -0
  19. package/dist/shared/schemas/entities.schema.js +262 -0
  20. package/dist/shared/schemas/enums.schema.d.ts +135 -0
  21. package/dist/shared/schemas/enums.schema.js +58 -0
  22. package/dist/shared/schemas/gui-api.schema.d.ts +53 -0
  23. package/dist/shared/schemas/gui-api.schema.js +20 -0
  24. package/dist/shared/schemas/ideas-api.schema.d.ts +81 -0
  25. package/dist/shared/schemas/ideas-api.schema.js +40 -0
  26. package/dist/shared/schemas/notifications-api.schema.d.ts +87 -0
  27. package/dist/shared/schemas/notifications-api.schema.js +58 -0
  28. package/dist/shared/schemas/tasks-api.schema.d.ts +675 -0
  29. package/dist/shared/schemas/tasks-api.schema.js +225 -0
  30. package/dist/shared/schemas/tokens-api.schema.d.ts +106 -0
  31. package/dist/shared/schemas/tokens-api.schema.js +42 -0
  32. package/dist/shared/schemas/transcription-api.schema.d.ts +15 -0
  33. package/dist/shared/schemas/transcription-api.schema.js +12 -0
  34. package/dist/src/agent-api/checklist.d.ts +3 -0
  35. package/dist/src/agent-api/checklist.js +127 -0
  36. package/dist/src/agent-api/ideas.d.ts +3 -0
  37. package/dist/src/agent-api/ideas.js +93 -0
  38. package/dist/src/agent-api/notifications.d.ts +16 -0
  39. package/dist/src/agent-api/notifications.js +91 -0
  40. package/dist/src/agent-api/requests.d.ts +3 -0
  41. package/dist/src/agent-api/requests.js +60 -0
  42. package/dist/src/agent-api/router.d.ts +3 -0
  43. package/dist/src/agent-api/router.js +41 -0
  44. package/dist/src/agent-api/tasks.d.ts +2 -0
  45. package/dist/src/agent-api/tasks.js +290 -0
  46. package/dist/src/agent-api/types.d.ts +41 -0
  47. package/dist/src/agent-api/types.js +14 -0
  48. package/dist/src/cli/backup.d.ts +12 -0
  49. package/dist/src/cli/backup.js +126 -0
  50. package/dist/src/cli/prompt.d.ts +7 -0
  51. package/dist/src/cli/prompt.js +40 -0
  52. package/dist/src/cli/set-user.d.ts +2 -0
  53. package/dist/src/cli/set-user.js +114 -0
  54. package/dist/src/cli/setup-mcp.d.ts +10 -0
  55. package/dist/src/cli/setup-mcp.js +110 -0
  56. package/dist/src/cli/setup.d.ts +39 -0
  57. package/dist/src/cli/setup.js +626 -0
  58. package/dist/src/cli/watch.d.ts +4 -0
  59. package/dist/src/cli/watch.js +62 -0
  60. package/dist/src/core/accounts.d.ts +129 -0
  61. package/dist/src/core/accounts.js +284 -0
  62. package/dist/src/core/agent-tokens.d.ts +31 -0
  63. package/dist/src/core/agent-tokens.js +120 -0
  64. package/dist/src/core/agent.d.ts +71 -0
  65. package/dist/src/core/agent.js +358 -0
  66. package/dist/src/core/attachments.d.ts +21 -0
  67. package/dist/src/core/attachments.js +44 -0
  68. package/dist/src/core/data-dir.d.ts +5 -0
  69. package/dist/src/core/data-dir.js +11 -0
  70. package/dist/src/core/db.d.ts +13 -0
  71. package/dist/src/core/db.js +38 -0
  72. package/dist/src/core/env-file.d.ts +11 -0
  73. package/dist/src/core/env-file.js +36 -0
  74. package/dist/src/core/errors.d.ts +24 -0
  75. package/dist/src/core/errors.js +42 -0
  76. package/dist/src/core/history.d.ts +34 -0
  77. package/dist/src/core/history.js +279 -0
  78. package/dist/src/core/ideas.d.ts +16 -0
  79. package/dist/src/core/ideas.js +100 -0
  80. package/dist/src/core/ids.d.ts +2 -0
  81. package/dist/src/core/ids.js +5 -0
  82. package/dist/src/core/log-dir.d.ts +2 -0
  83. package/dist/src/core/log-dir.js +9 -0
  84. package/dist/src/core/logger.d.ts +14 -0
  85. package/dist/src/core/logger.js +37 -0
  86. package/dist/src/core/mail.d.ts +43 -0
  87. package/dist/src/core/mail.js +44 -0
  88. package/dist/src/core/mcp-entry.d.ts +20 -0
  89. package/dist/src/core/mcp-entry.js +15 -0
  90. package/dist/src/core/migrations.d.ts +18 -0
  91. package/dist/src/core/migrations.js +404 -0
  92. package/dist/src/core/native-sqlite.d.ts +2 -0
  93. package/dist/src/core/native-sqlite.js +23 -0
  94. package/dist/src/core/notifications.d.ts +46 -0
  95. package/dist/src/core/notifications.js +173 -0
  96. package/dist/src/core/project-root.d.ts +2 -0
  97. package/dist/src/core/project-root.js +13 -0
  98. package/dist/src/core/requests.d.ts +50 -0
  99. package/dist/src/core/requests.js +240 -0
  100. package/dist/src/core/snapshots.d.ts +32 -0
  101. package/dist/src/core/snapshots.js +35 -0
  102. package/dist/src/core/spaces.d.ts +66 -0
  103. package/dist/src/core/spaces.js +252 -0
  104. package/dist/src/core/store.d.ts +577 -0
  105. package/dist/src/core/store.js +2119 -0
  106. package/dist/src/core/task-history.d.ts +7 -0
  107. package/dist/src/core/task-history.js +41 -0
  108. package/dist/src/core/tasks.d.ts +58 -0
  109. package/dist/src/core/tasks.js +389 -0
  110. package/dist/src/core/test-tasks.d.ts +19 -0
  111. package/dist/src/core/test-tasks.js +135 -0
  112. package/dist/src/core/transcription.d.ts +45 -0
  113. package/dist/src/core/transcription.js +92 -0
  114. package/dist/src/core/watchers.d.ts +30 -0
  115. package/dist/src/core/watchers.js +50 -0
  116. package/dist/src/deploy/env-render.d.ts +26 -0
  117. package/dist/src/deploy/env-render.js +98 -0
  118. package/dist/src/deploy/templates.d.ts +38 -0
  119. package/dist/src/deploy/templates.js +130 -0
  120. package/dist/src/gui-api.d.ts +37 -0
  121. package/dist/src/gui-api.js +383 -0
  122. package/dist/src/index.d.ts +2 -0
  123. package/dist/src/index.js +110 -0
  124. package/dist/src/mcp/client.d.ts +38 -0
  125. package/dist/src/mcp/client.js +106 -0
  126. package/dist/src/mcp/notifications.d.ts +13 -0
  127. package/dist/src/mcp/notifications.js +54 -0
  128. package/dist/src/mcp/start.d.ts +14 -0
  129. package/dist/src/mcp/start.js +123 -0
  130. package/dist/src/mcp/tools.d.ts +46 -0
  131. package/dist/src/mcp/tools.js +348 -0
  132. package/dist/src/mcp/watch-handoff.d.ts +35 -0
  133. package/dist/src/mcp/watch-handoff.js +91 -0
  134. package/dist/src/server/app.d.ts +25 -0
  135. package/dist/src/server/app.js +305 -0
  136. package/dist/src/server/config.d.ts +23 -0
  137. package/dist/src/server/config.js +75 -0
  138. package/dist/src/server/limits.d.ts +2 -0
  139. package/dist/src/server/limits.js +4 -0
  140. package/dist/src/server/login-limiter.d.ts +36 -0
  141. package/dist/src/server/login-limiter.js +81 -0
  142. package/dist/src/server/start.d.ts +5 -0
  143. package/dist/src/server/start.js +88 -0
  144. package/dist/src/version.d.ts +1 -0
  145. package/dist/src/version.js +16 -0
  146. package/gui/dist/assets/index-BVN0agCc.css +1 -0
  147. package/gui/dist/assets/index-DI1_kr81.js +70 -0
  148. package/gui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  149. package/gui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  150. package/gui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  151. package/gui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  152. package/gui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  153. package/gui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  154. package/gui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  155. package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  156. package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  157. package/gui/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  158. package/gui/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  159. package/gui/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  160. package/gui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  161. package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  162. package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  163. package/gui/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  164. package/gui/dist/index.html +13 -0
  165. package/package.json +66 -4
@@ -0,0 +1,54 @@
1
+ import { NotificationsDataSchema } from "../../shared/schemas/agent-tools.schema.js";
2
+ import { writeHandoff } from "./watch-handoff.js";
3
+ /**
4
+ * Outil `enable_notifications`, exécuté par le mode MCP lui-même : il lit le
5
+ * jeton, l'espace et l'état du veilleur sur le site, dépose le relais du
6
+ * veilleur et rend l'appel `Monitor` que l'agent doit faire.
7
+ */
8
+ /** Durée maximale d'un `Monitor` côté hôte. */
9
+ export const MONITOR_TIMEOUT_MS = 1_800_000;
10
+ const PACKAGE_NAME = "@imenam/mcp-organizer";
11
+ export const ENABLE_NOTIFICATIONS_TOOL = {
12
+ name: "enable_notifications",
13
+ description: "Get notified in real time of the requests the members of your space send to the AI, without polling the queue. Returns immediately: it blocks nothing and starts nothing on its own. It hands you the exact Monitor call to make right after — arming that watcher is what actually delivers notifications, and until you do it no request will reach you. Call this once at the start of a session; then keep working, each new pending request interrupts you on its own. The host stops the watcher after 30 minutes at most: re-arm it with the same Monitor call each time it expires.",
14
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
15
+ };
16
+ /** Commande du veilleur du jeton `tokenId`. */
17
+ export function watchCommand(tokenId) {
18
+ return `npx -y ${PACKAGE_NAME} --watch ${tokenId}`;
19
+ }
20
+ export async function enableNotifications(client, connection) {
21
+ const envelope = await client.envelope({ method: "GET", path: "notifications", query: {}, body: undefined });
22
+ const { token, space, pendingCount, watcher } = client.parseData(NotificationsDataSchema, envelope.data);
23
+ writeHandoff({ tokenId: token.id, url: connection.url, token: connection.token });
24
+ const command = watchCommand(token.id);
25
+ if (watcher.armed) {
26
+ return [
27
+ `Notifications are already live for token "${token.name}" on space "${space.name}" (watcher armed since ${watcher.since}).`,
28
+ "Nothing to do: keep working, new requests will interrupt you on their own.",
29
+ "Do not arm a second watcher — you would get every request twice.",
30
+ "Only exception: if the host just told you your watcher expired or stopped, re-arm it",
31
+ `with the same Monitor call (command \`${command}\`, timeout_ms ${MONITOR_TIMEOUT_MS}).`,
32
+ ].join("\n");
33
+ }
34
+ const lines = [
35
+ `Notifications enabled for token "${token.name}" on space "${space.name}".`,
36
+ "",
37
+ "Last step — call the Monitor tool now, with exactly these arguments:",
38
+ ` command: ${command}`,
39
+ " description: New requests for the AI in the organizer",
40
+ ` timeout_ms: ${MONITOR_TIMEOUT_MS}`,
41
+ "",
42
+ "Until that watcher is armed, nothing will reach you: no request will be announced.",
43
+ "Once it is, keep working normally — each request that becomes pending shows up on its own,",
44
+ "even while you are idle. One line per request; process it with claim_request.",
45
+ "",
46
+ "The host stops every monitor after 30 minutes at most. Each time it tells you this",
47
+ "watcher expired, call Monitor again right away with the same arguments — do not call",
48
+ "enable_notifications again. Skip that and notifications stop for good.",
49
+ ];
50
+ if (pendingCount > 0) {
51
+ lines.push("", `${pendingCount} request(s) are already pending. Only requests that become pending AFTER the`, "watcher starts are announced — process the backlog with list_requests.");
52
+ }
53
+ return lines.join("\n");
54
+ }
@@ -0,0 +1,14 @@
1
+ import { type SiteConnection } from "./client.js";
2
+ /** Présentation du serveur transmise au client MCP : concepts et déroulé. */
3
+ export declare const INSTRUCTIONS = "mcp-organizer connects you to an organizer site: a multi-user task manager where people organize tasks in a web app and queue requests for the AI. You, the connected agent, process those requests when the user asks you to. The site never calls an LLM: you are the intelligence.\n\nYour token covers exactly one space, with the rights of the user who created the token. Every tool works on that space.\n\nConcepts:\n- A **space** holds tasks and an idea box. A user's **personal space** is private; a **shared space** has one owner and members.\n- A **task** has a title, a markdown description, a status (todo, doing, review, done), a priority (low, medium, high, critical), assignees and tags. Tasks nest on two levels: task > sub-task > sub-sub-task. A task is either standard (with a checklist) or a test (with checkpoints that each tester marks ok, ko, blocked or untested).\n- A **request** is something a member of your space asked the AI: an idea, a remark, a test review, a message or an idea promotion. It is pending, then processing once you claim it (only your token can then act for it), then done (with your reply) or failed (with the reason).\n- A **batch** is the set of actions you perform for one request. Every action is logged with its before and after state and with your token, and users can undo one action or the whole batch.\n- The **scope** of your work is the space of your token: you see and process the requests of all its members, and you never act outside it.\n\nWork loop, when the user asks you to process the queue:\n1. list_requests to see the pending requests of your space.\n2. claim_request on the oldest one: it becomes processing for you and you get its context (text, author, context task, task tree, members, recent ideas). get_context shows the space at any time.\n3. Read what you need with get_task and search_tasks.\n4. Act with create_task, update_task, set_assignees, set_tags, edit_checklist, add_comment, create_idea, update_idea, promote_idea \u2014 always passing the request_id. Each write is one undoable action of the batch.\n5. complete_request with a short reply in the author's language (or fail_request with the reason), then go back to step 1 until no request is pending.\nWhen the queue is empty, tell the user.\n\nNotifications: call enable_notifications once at the start of the session and arm the watcher it hands you with Monitor. Each request that becomes pending in your space then arrives on its own as one line (kind, author, excerpt, request id), even while you are idle: process it with the work loop above, starting with claim_request. The host stops that watcher after 30 minutes at most: each time it tells you the watcher expired, re-arm it at once with the same Monitor call; otherwise notifications stop for good. A line \"notifications stopped\" means the site refused the token: tell the user.\n\nHow to handle each kind of request:\n- **idea**: classify it. If it belongs to an existing task or sub-task, attach it there (sub-task, checklist item or comment). If it is actionable and important enough, create a task (or sub-task) with a fitting priority. Otherwise, put it in the idea box with create_idea. Prefer existing tasks over duplicates.\n- **test_review**: feed the tester's findings back into the existing tasks (status, checklist, comments, priority of the affected tasks), or create a task for each problem that has none.\n- **remark** and **message**: do whatever is useful \u2014 answer, update or create tasks, comment \u2014 and say what you did in the reply.\n- **idea_promotion**: turn the context idea into a well-formed task with promote_idea.\n\nRules: write titles, descriptions and replies in the language of the users; keep the reply short and factual. Errors tell you what to do next; an error about the token (missing, revoked, expired, owner no longer a member) cannot be fixed by retrying: tell the user.";
4
+ /**
5
+ * Connexion au site d'après `ORGANIZER_URL` et `ORGANIZER_TOKEN` ; une valeur
6
+ * absente ou invalide, ou une URL http vers une autre machine, est une `ConfigError`.
7
+ */
8
+ export declare function loadSiteConnection(): SiteConnection;
9
+ /**
10
+ * Mode MCP (par défaut) : serveur stdio sans base ni logique métier. Il
11
+ * expose les outils de l'agent et relaie chaque appel à l'API agent du site
12
+ * désigné par `ORGANIZER_URL`, avec le jeton `ORGANIZER_TOKEN`.
13
+ */
14
+ export declare function runMcpMode(): Promise<void>;
@@ -0,0 +1,123 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import { APP_VERSION } from "../version.js";
5
+ import { createLogger, setupLogging } from "../core/logger.js";
6
+ import { ConfigError } from "../core/errors.js";
7
+ import { OrganizerSiteClient, SiteError } from "./client.js";
8
+ import { AGENT_TOOLS, ToolArgumentError, findAgentTool, toolHttpRequest } from "./tools.js";
9
+ import { ENABLE_NOTIFICATIONS_TOOL, enableNotifications } from "./notifications.js";
10
+ import { removeOwnHandoffs } from "./watch-handoff.js";
11
+ /** Présentation du serveur transmise au client MCP : concepts et déroulé. */
12
+ export const INSTRUCTIONS = `mcp-organizer connects you to an organizer site: a multi-user task manager where people organize tasks in a web app and queue requests for the AI. You, the connected agent, process those requests when the user asks you to. The site never calls an LLM: you are the intelligence.
13
+
14
+ Your token covers exactly one space, with the rights of the user who created the token. Every tool works on that space.
15
+
16
+ Concepts:
17
+ - A **space** holds tasks and an idea box. A user's **personal space** is private; a **shared space** has one owner and members.
18
+ - A **task** has a title, a markdown description, a status (todo, doing, review, done), a priority (low, medium, high, critical), assignees and tags. Tasks nest on two levels: task > sub-task > sub-sub-task. A task is either standard (with a checklist) or a test (with checkpoints that each tester marks ok, ko, blocked or untested).
19
+ - A **request** is something a member of your space asked the AI: an idea, a remark, a test review, a message or an idea promotion. It is pending, then processing once you claim it (only your token can then act for it), then done (with your reply) or failed (with the reason).
20
+ - A **batch** is the set of actions you perform for one request. Every action is logged with its before and after state and with your token, and users can undo one action or the whole batch.
21
+ - The **scope** of your work is the space of your token: you see and process the requests of all its members, and you never act outside it.
22
+
23
+ Work loop, when the user asks you to process the queue:
24
+ 1. list_requests to see the pending requests of your space.
25
+ 2. claim_request on the oldest one: it becomes processing for you and you get its context (text, author, context task, task tree, members, recent ideas). get_context shows the space at any time.
26
+ 3. Read what you need with get_task and search_tasks.
27
+ 4. Act with create_task, update_task, set_assignees, set_tags, edit_checklist, add_comment, create_idea, update_idea, promote_idea — always passing the request_id. Each write is one undoable action of the batch.
28
+ 5. complete_request with a short reply in the author's language (or fail_request with the reason), then go back to step 1 until no request is pending.
29
+ When the queue is empty, tell the user.
30
+
31
+ Notifications: call enable_notifications once at the start of the session and arm the watcher it hands you with Monitor. Each request that becomes pending in your space then arrives on its own as one line (kind, author, excerpt, request id), even while you are idle: process it with the work loop above, starting with claim_request. The host stops that watcher after 30 minutes at most: each time it tells you the watcher expired, re-arm it at once with the same Monitor call; otherwise notifications stop for good. A line "notifications stopped" means the site refused the token: tell the user.
32
+
33
+ How to handle each kind of request:
34
+ - **idea**: classify it. If it belongs to an existing task or sub-task, attach it there (sub-task, checklist item or comment). If it is actionable and important enough, create a task (or sub-task) with a fitting priority. Otherwise, put it in the idea box with create_idea. Prefer existing tasks over duplicates.
35
+ - **test_review**: feed the tester's findings back into the existing tasks (status, checklist, comments, priority of the affected tasks), or create a task for each problem that has none.
36
+ - **remark** and **message**: do whatever is useful — answer, update or create tasks, comment — and say what you did in the reply.
37
+ - **idea_promotion**: turn the context idea into a well-formed task with promote_idea.
38
+
39
+ Rules: write titles, descriptions and replies in the language of the users; keep the reply short and factual. Errors tell you what to do next; an error about the token (missing, revoked, expired, owner no longer a member) cannot be fixed by retrying: tell the user.`;
40
+ /** Hôtes de la machine locale, seuls admis en http. */
41
+ const LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1", "[::1]"]);
42
+ /** Variable d'environnement non vide, ou null. */
43
+ function read(name) {
44
+ const value = process.env[name]?.trim();
45
+ return value ? value : null;
46
+ }
47
+ /**
48
+ * Connexion au site d'après `ORGANIZER_URL` et `ORGANIZER_TOKEN` ; une valeur
49
+ * absente ou invalide, ou une URL http vers une autre machine, est une `ConfigError`.
50
+ */
51
+ export function loadSiteConnection() {
52
+ const rawUrl = read("ORGANIZER_URL");
53
+ const token = read("ORGANIZER_TOKEN");
54
+ const missing = [rawUrl === null ? "ORGANIZER_URL" : null, token === null ? "ORGANIZER_TOKEN" : null].filter((name) => name !== null);
55
+ if (missing.length > 0) {
56
+ throw new ConfigError(`mcp-organizer (MCP mode) needs ${missing.join(" and ")}: ORGANIZER_URL is the public URL of the organizer site, ORGANIZER_TOKEN an agent token created on that site (Paramètres > Agents). Set them in the "env" block of the MCP server declaration, or run: npx -y @imenam/mcp-organizer --claude-setup-mcp --url=<site URL> --token=<agent token>.`);
57
+ }
58
+ let url;
59
+ try {
60
+ url = new URL(rawUrl);
61
+ }
62
+ catch {
63
+ throw new ConfigError(`ORGANIZER_URL is not an absolute URL (${rawUrl}): expected the public URL of the site, e.g. https://organizer.example.com.`);
64
+ }
65
+ if (url.protocol !== "http:" && url.protocol !== "https:")
66
+ throw new ConfigError(`ORGANIZER_URL must use http or https (${rawUrl}).`);
67
+ if (url.search || url.hash || url.username || url.password)
68
+ throw new ConfigError(`ORGANIZER_URL must not contain credentials, a query or a fragment (${rawUrl}).`);
69
+ if (url.protocol === "http:" && !LOCAL_HOSTNAMES.has(url.hostname)) {
70
+ throw new ConfigError(`ORGANIZER_URL must use https for a remote site (${url.hostname}): over http the agent token would travel in clear text. http is only accepted for a site on this machine (localhost, 127.0.0.1, ::1). Use the https URL of the site.`);
71
+ }
72
+ return { url: url.href.replace(/\/+$/, ""), token: token };
73
+ }
74
+ /**
75
+ * Mode MCP (par défaut) : serveur stdio sans base ni logique métier. Il
76
+ * expose les outils de l'agent et relaie chaque appel à l'API agent du site
77
+ * désigné par `ORGANIZER_URL`, avec le jeton `ORGANIZER_TOKEN`.
78
+ */
79
+ export async function runMcpMode() {
80
+ const connection = loadSiteConnection();
81
+ const logDir = read("MCP_LOG_DIR");
82
+ if (logDir)
83
+ setupLogging("mcp-organizer", logDir);
84
+ const logger = createLogger("mcp");
85
+ const client = new OrganizerSiteClient(connection);
86
+ const server = new Server({ name: "mcp-organizer", version: APP_VERSION }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
87
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [...AGENT_TOOLS.map((tool) => tool.definition), ENABLE_NOTIFICATIONS_TOOL] }));
88
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
89
+ const { name, arguments: args } = request.params;
90
+ if (name === ENABLE_NOTIFICATIONS_TOOL.name) {
91
+ try {
92
+ return { content: [{ type: "text", text: await enableNotifications(client, connection) }] };
93
+ }
94
+ catch (error) {
95
+ if (!(error instanceof SiteError))
96
+ logger.error(`Tool ${name} failed: ${error?.stack ?? error}`);
97
+ return errorResult(error?.message ?? String(error));
98
+ }
99
+ }
100
+ const tool = findAgentTool(name);
101
+ if (!tool)
102
+ return errorResult(`Unknown tool "${name}". Call tools/list to get the available tools.`);
103
+ try {
104
+ const text = await client.call(toolHttpRequest(tool, (args ?? {})));
105
+ return { content: [{ type: "text", text }] };
106
+ }
107
+ catch (error) {
108
+ if (!(error instanceof SiteError) && !(error instanceof ToolArgumentError))
109
+ logger.error(`Tool ${name} failed: ${error?.stack ?? error}`);
110
+ return errorResult(error?.message ?? String(error));
111
+ }
112
+ });
113
+ // Le relais du veilleur porte le jeton : il ne survit pas au mode MCP qui l'a écrit.
114
+ process.on("exit", removeOwnHandoffs);
115
+ for (const signal of ["SIGINT", "SIGTERM"]) {
116
+ process.on(signal, () => process.exit(0));
117
+ }
118
+ await server.connect(new StdioServerTransport());
119
+ logger.info(`MCP server running on stdio (version ${APP_VERSION}, site: ${connection.url})`);
120
+ }
121
+ function errorResult(text) {
122
+ return { content: [{ type: "text", text: `Error: ${text}` }], isError: true };
123
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Outils de l'agent : définition (schéma JSON écrit à la main, description
3
+ * en anglais) et route de l'API agent du site qui l'exécute. Le mode MCP ne
4
+ * fait que relayer chaque appel à sa route.
5
+ */
6
+ /** Définition d'un outil telle que la liste `tools/list`. */
7
+ export interface ToolDefinition {
8
+ name: string;
9
+ description: string;
10
+ inputSchema: {
11
+ type: "object";
12
+ properties: Record<string, unknown>;
13
+ required?: string[];
14
+ additionalProperties?: boolean;
15
+ };
16
+ }
17
+ export interface AgentTool {
18
+ definition: ToolDefinition;
19
+ method: "GET" | "POST" | "PUT" | "PATCH";
20
+ /**
21
+ * Chemin sous `/api/agent/`. `{nom}` y est remplacé par l'argument `nom`,
22
+ * encodé ; les autres arguments forment la requête d'un GET, le corps JSON sinon.
23
+ */
24
+ path: string;
25
+ }
26
+ /** Requête HTTP d'un appel d'outil. */
27
+ export interface ToolHttpRequest {
28
+ method: AgentTool["method"];
29
+ /** Chemin sous `/api/agent/`, segments encodés. */
30
+ path: string;
31
+ query: Record<string, string>;
32
+ body: Record<string, unknown> | undefined;
33
+ }
34
+ /** Argument d'appel d'outil inutilisable pour former la requête. */
35
+ export declare class ToolArgumentError extends Error {
36
+ constructor(message: string);
37
+ }
38
+ export declare const AGENT_TOOLS: readonly AgentTool[];
39
+ /** Outil d'après son nom. */
40
+ export declare function findAgentTool(name: string): AgentTool | undefined;
41
+ /**
42
+ * Requête HTTP d'un appel d'outil : les arguments nommés dans le chemin y
43
+ * sont placés (ils sont alors obligatoires et textuels), les autres vont
44
+ * dans la requête d'un GET ou le corps JSON. Le site valide le reste.
45
+ */
46
+ export declare function toolHttpRequest(tool: AgentTool, args: Record<string, unknown>): ToolHttpRequest;
@@ -0,0 +1,348 @@
1
+ import { REQUEST_STATUSES, TASK_PRIORITIES, TASK_STATUSES, TASK_TYPES } from "../../shared/schemas/enums.schema.js";
2
+ import { MAX_TAG_LENGTH, MAX_TASK_TITLE_LENGTH } from "../../shared/schemas/tasks-api.schema.js";
3
+ import { MAX_CHECKLIST_ITEM_LENGTH, MAX_CHECKLIST_OPERATIONS, MAX_COMMENT_LENGTH, MAX_IDEA_LENGTH, MAX_REASON_LENGTH, MAX_REPLY_LENGTH, MAX_REQUEST_LIST_LIMIT, MAX_SEARCH_LIMIT, } from "../../shared/schemas/agent-tools.schema.js";
4
+ /** Argument d'appel d'outil inutilisable pour former la requête. */
5
+ export class ToolArgumentError extends Error {
6
+ constructor(message) {
7
+ super(message);
8
+ this.name = "ToolArgumentError";
9
+ }
10
+ }
11
+ const REQUEST_ID = {
12
+ type: "string",
13
+ description: "Id of the request you claimed with claim_request (status processing). Every write is recorded in its batch and can be undone by the users.",
14
+ };
15
+ const TASK_REF = { type: "string", description: "Task of your space: its key (e.g. ORG-41) or its id." };
16
+ export const AGENT_TOOLS = [
17
+ {
18
+ method: "GET",
19
+ path: "requests",
20
+ definition: {
21
+ name: "list_requests",
22
+ description: "List the requests the members of your space queued for the agent, with author, kind and context task: pending requests oldest first, other statuses most recent first. Call it when the user asks you to process the queue (default: pending), or to review processing, done, failed or cancelled requests.",
23
+ inputSchema: {
24
+ type: "object",
25
+ properties: {
26
+ status: { type: "string", enum: [...REQUEST_STATUSES, "all"], description: "Status to list. Default: pending." },
27
+ limit: { type: "integer", minimum: 1, maximum: MAX_REQUEST_LIST_LIMIT, description: "Maximum number of requests. Default: 20." },
28
+ },
29
+ additionalProperties: false,
30
+ },
31
+ },
32
+ },
33
+ {
34
+ method: "POST",
35
+ path: "requests/{request_id}/claim",
36
+ definition: {
37
+ name: "claim_request",
38
+ description: "Claim a pending request before working on it: it becomes processing for your token only (no other agent can take it or act for it) and you get its full context — text, author, context task or idea, the task tree of your space with keys, statuses, priorities and assignees, its members and recent ideas. Every write tool then needs this request_id.",
39
+ inputSchema: {
40
+ type: "object",
41
+ properties: { request_id: { type: "string", description: "Id of a pending request, from list_requests." } },
42
+ required: ["request_id"],
43
+ additionalProperties: false,
44
+ },
45
+ },
46
+ },
47
+ {
48
+ method: "GET",
49
+ path: "context",
50
+ definition: {
51
+ name: "get_context",
52
+ description: "Show your space: its name and task key prefix, its members (ids and e-mails, for set_assignees), the task tree with statuses, priorities and assignees, and the recent ideas of its idea box.",
53
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
54
+ },
55
+ },
56
+ {
57
+ method: "GET",
58
+ path: "tasks/{task}",
59
+ definition: {
60
+ name: "get_task",
61
+ description: "Read one task of your space in detail: fields, parents, assignees, tags, description, checklist items or test checkpoints (with their ids, needed by edit_checklist), sub-tasks, recent comments and attachments. Use it before modifying a task.",
62
+ inputSchema: { type: "object", properties: { task: TASK_REF }, required: ["task"], additionalProperties: false },
63
+ },
64
+ },
65
+ {
66
+ method: "GET",
67
+ path: "tasks",
68
+ definition: {
69
+ name: "search_tasks",
70
+ description: "Search the tasks of your space by words of their title, description or key, optionally filtered by status, priority, type or assignee. Use it to find an existing task to attach an idea to or to update, before creating a duplicate.",
71
+ inputSchema: {
72
+ type: "object",
73
+ properties: {
74
+ query: { type: "string", description: "Words to look for (case-insensitive, all must match). Omit to list by filters only." },
75
+ status: { type: "string", enum: [...TASK_STATUSES] },
76
+ priority: { type: "string", enum: [...TASK_PRIORITIES] },
77
+ type: { type: "string", enum: [...TASK_TYPES] },
78
+ assignee: { type: "string", description: "Member id or e-mail." },
79
+ limit: { type: "integer", minimum: 1, maximum: MAX_SEARCH_LIMIT, description: "Maximum number of results. Default: 30." },
80
+ },
81
+ additionalProperties: false,
82
+ },
83
+ },
84
+ },
85
+ {
86
+ method: "POST",
87
+ path: "tasks",
88
+ definition: {
89
+ name: "create_task",
90
+ description: "Create a task in your space: a top-level task, or a sub-task / sub-sub-task with parent (two levels at most). Use type test for a task whose checkpoints testers verify. Search first with search_tasks to avoid duplicates. It goes at the bottom of its column.",
91
+ inputSchema: {
92
+ type: "object",
93
+ properties: {
94
+ request_id: REQUEST_ID,
95
+ title: { type: "string", maxLength: MAX_TASK_TITLE_LENGTH, description: "Short, actionable title, in the language of the space." },
96
+ description: { type: "string", description: "Markdown description." },
97
+ type: { type: "string", enum: [...TASK_TYPES], description: "Default: standard." },
98
+ priority: { type: "string", enum: [...TASK_PRIORITIES], description: "Default: medium." },
99
+ status: { type: "string", enum: [...TASK_STATUSES], description: "Default: todo." },
100
+ parent: { type: "string", description: "Parent task key (e.g. ORG-41) or id, to create a sub-task." },
101
+ },
102
+ required: ["request_id", "title"],
103
+ additionalProperties: false,
104
+ },
105
+ },
106
+ },
107
+ {
108
+ method: "PATCH",
109
+ path: "tasks/{task}",
110
+ definition: {
111
+ name: "update_task",
112
+ description: "Change a task of your space: title, description, status, priority, type, parent (move it under another task, or null to make it top-level), position in its column (after_task) and, for a test task, the task it tests (tested_task_id). Only the given fields change. Read the task with get_task first.",
113
+ inputSchema: {
114
+ type: "object",
115
+ properties: {
116
+ request_id: REQUEST_ID,
117
+ task: TASK_REF,
118
+ title: { type: "string", maxLength: MAX_TASK_TITLE_LENGTH },
119
+ description: { type: "string", description: "New markdown description (replaces the whole description)." },
120
+ status: { type: "string", enum: [...TASK_STATUSES] },
121
+ priority: { type: "string", enum: [...TASK_PRIORITIES] },
122
+ type: { type: "string", enum: [...TASK_TYPES] },
123
+ parent: { type: ["string", "null"], description: "New parent task key or id; null makes the task top-level." },
124
+ after_task: {
125
+ type: ["string", "null"],
126
+ description: "Place the task right after this task of its (new) column — same parent and status; null puts it at the top of the column.",
127
+ },
128
+ tested_task_id: {
129
+ type: ["string", "null"],
130
+ description: "Test tasks only: the task of your space this test verifies (key or id, not the test itself); null removes the link. A task changed to standard loses it.",
131
+ },
132
+ },
133
+ required: ["request_id", "task"],
134
+ additionalProperties: false,
135
+ },
136
+ },
137
+ },
138
+ {
139
+ method: "PUT",
140
+ path: "tasks/{task}/assignees",
141
+ definition: {
142
+ name: "set_assignees",
143
+ description: "Replace the assignees of a task of your space (the testers, for a test task). Pass the full list: members not listed are unassigned, an empty list unassigns everybody. Newly assigned members are notified. Member ids and e-mails are listed by claim_request and get_context.",
144
+ inputSchema: {
145
+ type: "object",
146
+ properties: {
147
+ request_id: REQUEST_ID,
148
+ task: TASK_REF,
149
+ assignees: { type: "array", items: { type: "string" }, description: "Member ids or e-mails: the complete new list." },
150
+ },
151
+ required: ["request_id", "task", "assignees"],
152
+ additionalProperties: false,
153
+ },
154
+ },
155
+ },
156
+ {
157
+ method: "PUT",
158
+ path: "tasks/{task}/tags",
159
+ definition: {
160
+ name: "set_tags",
161
+ description: "Replace the tags of a task of your space. Pass the full list; an unknown name creates the tag in the space (names match case-insensitively, so reuse existing tags), an empty list removes all tags.",
162
+ inputSchema: {
163
+ type: "object",
164
+ properties: {
165
+ request_id: REQUEST_ID,
166
+ task: TASK_REF,
167
+ tags: { type: "array", items: { type: "string", maxLength: MAX_TAG_LENGTH }, description: "Tag names: the complete new list." },
168
+ },
169
+ required: ["request_id", "task", "tags"],
170
+ additionalProperties: false,
171
+ },
172
+ },
173
+ },
174
+ {
175
+ method: "POST",
176
+ path: "tasks/{task}/checklist",
177
+ definition: {
178
+ name: "edit_checklist",
179
+ description: "Edit the checklist of a standard task (add, rename, check or uncheck, delete items) or the checkpoints of a test task (add, rename, delete; testers record the results themselves). Operations apply in order, all or nothing. Item ids come from get_task.",
180
+ inputSchema: {
181
+ type: "object",
182
+ properties: {
183
+ request_id: REQUEST_ID,
184
+ task: TASK_REF,
185
+ operations: {
186
+ type: "array",
187
+ minItems: 1,
188
+ maxItems: MAX_CHECKLIST_OPERATIONS,
189
+ items: {
190
+ type: "object",
191
+ properties: {
192
+ action: { type: "string", enum: ["add", "update", "delete"] },
193
+ text: { type: "string", maxLength: MAX_CHECKLIST_ITEM_LENGTH, description: "add: text of the new item; update: new text." },
194
+ item_id: { type: "string", description: "update / delete: id of the checklist item or checkpoint." },
195
+ done: { type: "boolean", description: "update, standard task only: check (true) or uncheck (false) the item." },
196
+ },
197
+ required: ["action"],
198
+ additionalProperties: false,
199
+ },
200
+ },
201
+ },
202
+ required: ["request_id", "task", "operations"],
203
+ additionalProperties: false,
204
+ },
205
+ },
206
+ },
207
+ {
208
+ method: "POST",
209
+ path: "tasks/{task}/comments",
210
+ definition: {
211
+ name: "add_comment",
212
+ description: "Comment on a task of your space as the agent (shown as written by the agent, in the name of your token's owner). Assignees and previous commenters are notified. Use it to record a remark, a decision or a test finding on the task it concerns.",
213
+ inputSchema: {
214
+ type: "object",
215
+ properties: {
216
+ request_id: REQUEST_ID,
217
+ task: TASK_REF,
218
+ body: { type: "string", maxLength: MAX_COMMENT_LENGTH, description: "Comment text, in the language of the space." },
219
+ },
220
+ required: ["request_id", "task", "body"],
221
+ additionalProperties: false,
222
+ },
223
+ },
224
+ },
225
+ {
226
+ method: "POST",
227
+ path: "ideas",
228
+ definition: {
229
+ name: "create_idea",
230
+ description: "Put an idea in the idea box of your space. Use it for an idea that is not urgent or not actionable yet and does not belong to an existing task; rewrite it as one clear sentence.",
231
+ inputSchema: {
232
+ type: "object",
233
+ properties: {
234
+ request_id: REQUEST_ID,
235
+ text: { type: "string", maxLength: MAX_IDEA_LENGTH, description: "The idea, clearly worded, in the language of the space." },
236
+ },
237
+ required: ["request_id", "text"],
238
+ additionalProperties: false,
239
+ },
240
+ },
241
+ },
242
+ {
243
+ method: "PATCH",
244
+ path: "ideas/{idea_id}",
245
+ definition: {
246
+ name: "update_idea",
247
+ description: "Reword an idea of your space's idea box, or archive it (archived true) when it is done, obsolete or merged into a task; archived false brings it back. Idea ids are listed by claim_request and get_context.",
248
+ inputSchema: {
249
+ type: "object",
250
+ properties: {
251
+ request_id: REQUEST_ID,
252
+ idea_id: { type: "string" },
253
+ text: { type: "string", maxLength: MAX_IDEA_LENGTH, description: "New wording." },
254
+ archived: { type: "boolean" },
255
+ },
256
+ required: ["request_id", "idea_id"],
257
+ additionalProperties: false,
258
+ },
259
+ },
260
+ },
261
+ {
262
+ method: "POST",
263
+ path: "ideas/{idea_id}/promote",
264
+ definition: {
265
+ name: "promote_idea",
266
+ description: "Turn an idea of your space's idea box into a task (todo column): the task is created with the given title, description, type, priority and optional parent, and the idea is marked as promoted to it. Use it for an idea_promotion request, or when an idea in the box has become actionable.",
267
+ inputSchema: {
268
+ type: "object",
269
+ properties: {
270
+ request_id: REQUEST_ID,
271
+ idea_id: { type: "string" },
272
+ title: { type: "string", maxLength: MAX_TASK_TITLE_LENGTH, description: "Short, actionable task title." },
273
+ description: { type: "string", description: "Markdown description, usually the idea reworded with what to do." },
274
+ type: { type: "string", enum: [...TASK_TYPES], description: "Default: standard." },
275
+ priority: { type: "string", enum: [...TASK_PRIORITIES], description: "Default: medium." },
276
+ parent: { type: "string", description: "Parent task key or id, to create a sub-task." },
277
+ },
278
+ required: ["request_id", "idea_id", "title"],
279
+ additionalProperties: false,
280
+ },
281
+ },
282
+ },
283
+ {
284
+ method: "POST",
285
+ path: "requests/{request_id}/complete",
286
+ definition: {
287
+ name: "complete_request",
288
+ description: "Finish a processing request with your reply to its author: one or two sentences in the author's language saying what you did (tasks created or changed, where the idea went) or answering their message. The reply is shown in the assistant panel and on the Agent page, next to the actions of the batch. Call it last.",
289
+ inputSchema: {
290
+ type: "object",
291
+ properties: {
292
+ request_id: { type: "string", description: "Id of the request you claimed." },
293
+ reply: { type: "string", maxLength: MAX_REPLY_LENGTH, description: "Short report for the author." },
294
+ },
295
+ required: ["request_id", "reply"],
296
+ additionalProperties: false,
297
+ },
298
+ },
299
+ },
300
+ {
301
+ method: "POST",
302
+ path: "requests/{request_id}/fail",
303
+ definition: {
304
+ name: "fail_request",
305
+ description: "Finish a processing request as failed when you cannot do it (unclear request, impossible change, your token's owner can no longer act in the space). Give the reason in the author's language: they see it. Actions already done stay in the batch and can be undone by the users.",
306
+ inputSchema: {
307
+ type: "object",
308
+ properties: {
309
+ request_id: { type: "string", description: "Id of the request you claimed." },
310
+ reason: { type: "string", maxLength: MAX_REASON_LENGTH, description: "Why the request could not be done, and what the author can do." },
311
+ },
312
+ required: ["request_id", "reason"],
313
+ additionalProperties: false,
314
+ },
315
+ },
316
+ },
317
+ ];
318
+ /** Outil d'après son nom. */
319
+ export function findAgentTool(name) {
320
+ return AGENT_TOOLS.find((tool) => tool.definition.name === name);
321
+ }
322
+ /**
323
+ * Requête HTTP d'un appel d'outil : les arguments nommés dans le chemin y
324
+ * sont placés (ils sont alors obligatoires et textuels), les autres vont
325
+ * dans la requête d'un GET ou le corps JSON. Le site valide le reste.
326
+ */
327
+ export function toolHttpRequest(tool, args) {
328
+ const rest = { ...args };
329
+ const path = tool.path.replace(/\{([a-z_]+)\}/g, (_, name) => {
330
+ const value = rest[name];
331
+ if (typeof value !== "string" || value.trim() === "") {
332
+ throw new ToolArgumentError(`Invalid arguments — ${name}: expected a non-empty string. Check the tool's input schema.`);
333
+ }
334
+ delete rest[name];
335
+ return encodeURIComponent(value.trim());
336
+ });
337
+ if (tool.method !== "GET")
338
+ return { method: tool.method, path, query: {}, body: rest };
339
+ const query = {};
340
+ for (const [name, value] of Object.entries(rest)) {
341
+ if (value === undefined || value === null)
342
+ continue;
343
+ if (typeof value === "object")
344
+ throw new ToolArgumentError(`Invalid arguments — ${name}: expected a single value. Check the tool's input schema.`);
345
+ query[name] = String(value);
346
+ }
347
+ return { method: tool.method, path, query, body: undefined };
348
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Relais entre la session MCP et son veilleur de notifications.
3
+ *
4
+ * Le veilleur est lancé par l'agent via l'outil `Monitor` de l'hôte : c'est un
5
+ * processus du shell de la session, pas un enfant du mode MCP. Il n'hérite donc
6
+ * pas du bloc `env` du `.mcp.json` et n'a ni URL du site ni jeton. Les passer sur
7
+ * la ligne de commande les afficherait dans la conversation : ils transitent par
8
+ * un fichier en 0600, que seul l'identifiant du jeton (pas sa valeur) permet de
9
+ * retrouver.
10
+ *
11
+ * Le fichier est écrit par `enable_notifications`, jamais au démarrage, et retiré
12
+ * à l'arrêt du mode MCP qui l'a écrit en dernier.
13
+ */
14
+ export interface Handoff {
15
+ /** Identifiant du jeton d'agent. */
16
+ tokenId: string;
17
+ url: string;
18
+ /** Valeur du jeton. */
19
+ token: string;
20
+ /** Processus du mode MCP qui a écrit le relais. */
21
+ pid: number;
22
+ /** Horodatage d'écriture, qui sert à purger les relais abandonnés. */
23
+ writtenAt: string;
24
+ }
25
+ /** Répertoire des relais : `<ORGANIZER_STATE_DIR>/watch`, ou `~/.mcp-organizer/watch`. */
26
+ export declare function handoffDir(): string;
27
+ /** Fichier du relais d'un jeton, nommé d'après le hachage de son identifiant. */
28
+ export declare function handoffPath(tokenId: string): string;
29
+ export declare function writeHandoff(handoff: Omit<Handoff, "pid" | "writtenAt">): string;
30
+ /** Relais du jeton `tokenId`, ou null s'il n'existe pas ou ne concerne pas ce jeton. */
31
+ export declare function readHandoff(tokenId: string): Handoff | null;
32
+ /** Supprime le relais du jeton s'il a été écrit en dernier par ce processus. */
33
+ export declare function removeHandoff(tokenId: string): void;
34
+ /** Supprime les relais écrits en dernier par ce processus. Synchrone : appelable sur `exit`. */
35
+ export declare function removeOwnHandoffs(): void;