mulmoclaude 0.1.0
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/README.md +44 -0
- package/bin/mulmoclaude.js +202 -0
- package/bin/prepare-dist.js +93 -0
- package/client/assets/chunk-vKJrgz-R-C_I3GbVV.js +1 -0
- package/client/assets/html2canvas-Cx501zZr-BF5dYYkY.js +5 -0
- package/client/assets/index-D8rhwXLq.js +4906 -0
- package/client/assets/index-KNLBjwuh.css +1 -0
- package/client/assets/index.es-D4YyL_Dg-BfRHLTZV.js +5 -0
- package/client/assets/material-icons-Dr0goTwe.woff +0 -0
- package/client/assets/material-icons-kAwBdRge.woff2 +0 -0
- package/client/assets/material-icons-outlined-BpWbwl2n.woff +0 -0
- package/client/assets/material-icons-outlined-DZhiGvEA.woff2 +0 -0
- package/client/assets/material-icons-round-BDlwx-sv.woff +0 -0
- package/client/assets/material-icons-round-DrirKXBx.woff2 +0 -0
- package/client/assets/material-icons-sharp-CH1KkVu7.woff +0 -0
- package/client/assets/material-icons-sharp-gidztirS.woff2 +0 -0
- package/client/assets/material-icons-two-tone-B7wz7mED.woff +0 -0
- package/client/assets/material-icons-two-tone-DuNIpaEj.woff2 +0 -0
- package/client/assets/mulmo_bw-ERmkSv0a.png +0 -0
- package/client/assets/purify.es-Fx1Nqyry-PeS5RUhs.js +2 -0
- package/client/assets/typeof-DBp4T-Ny-BC0P-2DM.js +1 -0
- package/client/index.html +28 -0
- package/package.json +66 -0
- package/server/agent/attachmentConverter.ts +270 -0
- package/server/agent/config.ts +414 -0
- package/server/agent/index.ts +260 -0
- package/server/agent/mcp-server.ts +412 -0
- package/server/agent/mcp-tools/index.ts +63 -0
- package/server/agent/mcp-tools/x.ts +188 -0
- package/server/agent/plugin-names.ts +75 -0
- package/server/agent/prompt.ts +349 -0
- package/server/agent/resumeFailover.ts +129 -0
- package/server/agent/sandboxMounts.ts +329 -0
- package/server/agent/stream.ts +194 -0
- package/server/api/auth/bearerAuth.ts +61 -0
- package/server/api/auth/token.ts +98 -0
- package/server/api/csrfGuard.ts +85 -0
- package/server/api/routes/agent.ts +478 -0
- package/server/api/routes/chart.ts +98 -0
- package/server/api/routes/chat-index.ts +46 -0
- package/server/api/routes/config.ts +258 -0
- package/server/api/routes/dispatchResponse.ts +79 -0
- package/server/api/routes/files.ts +812 -0
- package/server/api/routes/html.ts +101 -0
- package/server/api/routes/image.ts +169 -0
- package/server/api/routes/mulmo-script.ts +712 -0
- package/server/api/routes/mulmoScriptValidate.ts +101 -0
- package/server/api/routes/notifications.ts +69 -0
- package/server/api/routes/pdf.ts +163 -0
- package/server/api/routes/plugins.ts +276 -0
- package/server/api/routes/presentHtml.ts +48 -0
- package/server/api/routes/roles.ts +125 -0
- package/server/api/routes/scheduler.ts +153 -0
- package/server/api/routes/schedulerHandlers.ts +151 -0
- package/server/api/routes/schedulerTasks.ts +163 -0
- package/server/api/routes/sessions.ts +294 -0
- package/server/api/routes/sessionsCursor.ts +59 -0
- package/server/api/routes/skills.ts +195 -0
- package/server/api/routes/sources.ts +540 -0
- package/server/api/routes/todos.ts +263 -0
- package/server/api/routes/todosColumnsHandlers.ts +347 -0
- package/server/api/routes/todosHandlers.ts +274 -0
- package/server/api/routes/todosItemsHandlers.ts +386 -0
- package/server/api/routes/wiki/pageIndex.ts +53 -0
- package/server/api/routes/wiki.ts +363 -0
- package/server/api/sandboxStatus.ts +64 -0
- package/server/events/notifications.ts +160 -0
- package/server/events/pub-sub/index.ts +45 -0
- package/server/events/relay-client.ts +288 -0
- package/server/events/scheduler-adapter.ts +302 -0
- package/server/events/session-store/index.ts +492 -0
- package/server/events/task-manager/index.ts +181 -0
- package/server/index.ts +572 -0
- package/server/system/config.ts +243 -0
- package/server/system/credentials.ts +220 -0
- package/server/system/docker.ts +97 -0
- package/server/system/env.ts +109 -0
- package/server/system/logger/config.ts +112 -0
- package/server/system/logger/formatters.ts +40 -0
- package/server/system/logger/index.ts +53 -0
- package/server/system/logger/rotation.ts +37 -0
- package/server/system/logger/sinks.ts +101 -0
- package/server/system/logger/types.ts +29 -0
- package/server/utils/date.ts +57 -0
- package/server/utils/errors.ts +7 -0
- package/server/utils/fetch.ts +27 -0
- package/server/utils/files/atomic.ts +125 -0
- package/server/utils/files/html-io.ts +20 -0
- package/server/utils/files/image-store.ts +66 -0
- package/server/utils/files/index.ts +45 -0
- package/server/utils/files/journal-io.ts +213 -0
- package/server/utils/files/json.ts +69 -0
- package/server/utils/files/markdown-store.ts +33 -0
- package/server/utils/files/naming.ts +50 -0
- package/server/utils/files/reference-dirs-io.ts +45 -0
- package/server/utils/files/roles-io.ts +45 -0
- package/server/utils/files/safe.ts +106 -0
- package/server/utils/files/scheduler-io.ts +20 -0
- package/server/utils/files/scheduler-overrides-io.ts +64 -0
- package/server/utils/files/session-io.ts +136 -0
- package/server/utils/files/spreadsheet-store.ts +63 -0
- package/server/utils/files/todos-io.ts +29 -0
- package/server/utils/files/user-tasks-io.ts +25 -0
- package/server/utils/files/workspace-io.ts +221 -0
- package/server/utils/gemini.ts +59 -0
- package/server/utils/gitignore.ts +69 -0
- package/server/utils/http.ts +15 -0
- package/server/utils/httpError.ts +61 -0
- package/server/utils/id.ts +16 -0
- package/server/utils/json.ts +83 -0
- package/server/utils/logBackgroundError.ts +22 -0
- package/server/utils/markdown.ts +82 -0
- package/server/utils/request.ts +29 -0
- package/server/utils/slug.ts +50 -0
- package/server/utils/spawn.ts +62 -0
- package/server/utils/time.ts +34 -0
- package/server/utils/types.ts +47 -0
- package/server/workspace/chat-index/index.ts +153 -0
- package/server/workspace/chat-index/indexer.ts +209 -0
- package/server/workspace/chat-index/paths.ts +34 -0
- package/server/workspace/chat-index/summarizer.ts +247 -0
- package/server/workspace/chat-index/types.ts +38 -0
- package/server/workspace/custom-dirs.ts +220 -0
- package/server/workspace/helps/business.md +104 -0
- package/server/workspace/helps/github.md +23 -0
- package/server/workspace/helps/index.md +60 -0
- package/server/workspace/helps/mulmoscript.md +249 -0
- package/server/workspace/helps/sandbox.md +90 -0
- package/server/workspace/helps/spreadsheet.md +43 -0
- package/server/workspace/helps/telegram.md +135 -0
- package/server/workspace/helps/wiki.md +131 -0
- package/server/workspace/journal/archivist.ts +386 -0
- package/server/workspace/journal/dailyPass.ts +743 -0
- package/server/workspace/journal/diff.ts +71 -0
- package/server/workspace/journal/index.ts +185 -0
- package/server/workspace/journal/indexFile.ts +136 -0
- package/server/workspace/journal/linkRewrite.ts +4 -0
- package/server/workspace/journal/memoryExtractor.ts +130 -0
- package/server/workspace/journal/optimizationPass.ts +160 -0
- package/server/workspace/journal/paths.ts +76 -0
- package/server/workspace/journal/state.ts +125 -0
- package/server/workspace/paths.ts +158 -0
- package/server/workspace/reference-dirs.ts +252 -0
- package/server/workspace/roles.ts +37 -0
- package/server/workspace/skills/discovery.ts +125 -0
- package/server/workspace/skills/index.ts +10 -0
- package/server/workspace/skills/parser.ts +144 -0
- package/server/workspace/skills/paths.ts +41 -0
- package/server/workspace/skills/scheduler.ts +149 -0
- package/server/workspace/skills/types.ts +30 -0
- package/server/workspace/skills/user-tasks.ts +257 -0
- package/server/workspace/skills/writer.ts +189 -0
- package/server/workspace/sources/arxivDiscovery.ts +182 -0
- package/server/workspace/sources/classifier.ts +268 -0
- package/server/workspace/sources/fetchers/arxiv.ts +170 -0
- package/server/workspace/sources/fetchers/github.ts +106 -0
- package/server/workspace/sources/fetchers/githubIssues.ts +208 -0
- package/server/workspace/sources/fetchers/githubReleases.ts +186 -0
- package/server/workspace/sources/fetchers/index.ts +71 -0
- package/server/workspace/sources/fetchers/registerAll.ts +15 -0
- package/server/workspace/sources/fetchers/rss.ts +141 -0
- package/server/workspace/sources/fetchers/rssParser.ts +295 -0
- package/server/workspace/sources/httpFetcher.ts +230 -0
- package/server/workspace/sources/interests.ts +120 -0
- package/server/workspace/sources/paths.ts +110 -0
- package/server/workspace/sources/pipeline/dedup.ts +60 -0
- package/server/workspace/sources/pipeline/fetch.ts +136 -0
- package/server/workspace/sources/pipeline/index.ts +249 -0
- package/server/workspace/sources/pipeline/notify.ts +72 -0
- package/server/workspace/sources/pipeline/plan.ts +66 -0
- package/server/workspace/sources/pipeline/summarize.ts +189 -0
- package/server/workspace/sources/pipeline/write.ts +185 -0
- package/server/workspace/sources/rateLimiter.ts +148 -0
- package/server/workspace/sources/registry.ts +326 -0
- package/server/workspace/sources/robots.ts +271 -0
- package/server/workspace/sources/sourceState.ts +135 -0
- package/server/workspace/sources/taxonomy.ts +74 -0
- package/server/workspace/sources/types.ts +144 -0
- package/server/workspace/sources/urls.ts +112 -0
- package/server/workspace/tool-trace/classify.ts +114 -0
- package/server/workspace/tool-trace/index.ts +250 -0
- package/server/workspace/tool-trace/writeSearch.ts +98 -0
- package/server/workspace/wiki-backlinks/index.ts +107 -0
- package/server/workspace/wiki-backlinks/sessionBacklinks.ts +144 -0
- package/server/workspace/workspace.ts +66 -0
- package/src/App.vue +720 -0
- package/src/assets/mulmo_bw.png +0 -0
- package/src/components/CanvasViewToggle.vue +27 -0
- package/src/components/ChatAttachmentPreview.vue +45 -0
- package/src/components/ChatImagePreview.vue +17 -0
- package/src/components/ChatInput.vue +208 -0
- package/src/components/FileContentHeader.vue +49 -0
- package/src/components/FileContentRenderer.vue +162 -0
- package/src/components/FileTree.vue +115 -0
- package/src/components/FileTreePane.vue +85 -0
- package/src/components/FilesView.vue +206 -0
- package/src/components/LockStatusPopup.vue +111 -0
- package/src/components/NotificationBell.vue +131 -0
- package/src/components/NotificationToast.vue +72 -0
- package/src/components/PluginLauncher.vue +138 -0
- package/src/components/RightSidebar.vue +113 -0
- package/src/components/RoleSelector.vue +64 -0
- package/src/components/SessionHistoryPanel.vue +176 -0
- package/src/components/SessionTabBar.vue +81 -0
- package/src/components/SettingsMcpTab.vue +350 -0
- package/src/components/SettingsModal.vue +275 -0
- package/src/components/SettingsReferenceDirsTab.vue +173 -0
- package/src/components/SettingsWorkspaceDirsTab.vue +174 -0
- package/src/components/SidebarHeader.vue +69 -0
- package/src/components/StackView.vue +360 -0
- package/src/components/SuggestionsPanel.vue +65 -0
- package/src/components/TodoExplorer.vue +358 -0
- package/src/components/ToolResultsPanel.vue +77 -0
- package/src/components/todo/TodoAddDialog.vue +131 -0
- package/src/components/todo/TodoEditDialog.vue +47 -0
- package/src/components/todo/TodoEditPanel.vue +113 -0
- package/src/components/todo/TodoKanbanView.vue +249 -0
- package/src/components/todo/TodoListView.vue +79 -0
- package/src/components/todo/TodoTableView.vue +177 -0
- package/src/composables/useActiveSession.ts +40 -0
- package/src/composables/useAppApi.ts +45 -0
- package/src/composables/useCanvasViewMode.ts +121 -0
- package/src/composables/useChatScroll.ts +47 -0
- package/src/composables/useClickOutside.ts +26 -0
- package/src/composables/useClipboardCopy.ts +44 -0
- package/src/composables/useContentDisplay.ts +52 -0
- package/src/composables/useDebugBeat.ts +23 -0
- package/src/composables/useDynamicFavicon.ts +115 -0
- package/src/composables/useEventListeners.ts +42 -0
- package/src/composables/useExpandedDirs.ts +64 -0
- package/src/composables/useFaviconState.ts +30 -0
- package/src/composables/useFileSelection.ts +115 -0
- package/src/composables/useFileSortMode.ts +24 -0
- package/src/composables/useFileTree.ts +85 -0
- package/src/composables/useFreshPluginData.ts +89 -0
- package/src/composables/useHealth.ts +38 -0
- package/src/composables/useImeAwareEnter.ts +57 -0
- package/src/composables/useKeyNavigation.ts +60 -0
- package/src/composables/useMarkdownLinkHandler.ts +46 -0
- package/src/composables/useMarkdownMode.ts +17 -0
- package/src/composables/useMcpTools.ts +71 -0
- package/src/composables/useMergedSessions.ts +27 -0
- package/src/composables/useNotifications.ts +90 -0
- package/src/composables/usePdfDownload.ts +60 -0
- package/src/composables/usePendingCalls.ts +77 -0
- package/src/composables/usePubSub.ts +85 -0
- package/src/composables/useRightSidebar.ts +23 -0
- package/src/composables/useRoles.ts +34 -0
- package/src/composables/useSandboxStatus.ts +67 -0
- package/src/composables/useSelectedResult.ts +49 -0
- package/src/composables/useSessionDerived.ts +51 -0
- package/src/composables/useSessionHistory.ts +81 -0
- package/src/composables/useSessionSync.ts +57 -0
- package/src/composables/useViewLayout.ts +55 -0
- package/src/config/apiRoutes.ts +173 -0
- package/src/config/pubsubChannels.ts +45 -0
- package/src/config/roles.ts +335 -0
- package/src/config/schedulerActions.ts +25 -0
- package/src/config/toolNames.ts +71 -0
- package/src/config/workspacePaths.ts +24 -0
- package/src/index.css +107 -0
- package/src/main.ts +25 -0
- package/src/plugins/canvas/Preview.vue +13 -0
- package/src/plugins/canvas/View.vue +333 -0
- package/src/plugins/canvas/definition.ts +38 -0
- package/src/plugins/canvas/index.ts +36 -0
- package/src/plugins/chart/Preview.vue +49 -0
- package/src/plugins/chart/View.vue +143 -0
- package/src/plugins/chart/definition.ts +58 -0
- package/src/plugins/chart/index.ts +52 -0
- package/src/plugins/editImage/Preview.vue +13 -0
- package/src/plugins/editImage/View.vue +13 -0
- package/src/plugins/editImage/definition.ts +27 -0
- package/src/plugins/editImage/index.ts +36 -0
- package/src/plugins/generateImage/Preview.vue +13 -0
- package/src/plugins/generateImage/View.vue +33 -0
- package/src/plugins/generateImage/definition.ts +32 -0
- package/src/plugins/generateImage/index.ts +56 -0
- package/src/plugins/manageRoles/Preview.vue +49 -0
- package/src/plugins/manageRoles/View.vue +525 -0
- package/src/plugins/manageRoles/definition.ts +43 -0
- package/src/plugins/manageRoles/index.ts +47 -0
- package/src/plugins/manageSkills/Preview.vue +21 -0
- package/src/plugins/manageSkills/View.vue +321 -0
- package/src/plugins/manageSkills/definition.ts +49 -0
- package/src/plugins/manageSkills/index.ts +49 -0
- package/src/plugins/manageSource/Preview.vue +33 -0
- package/src/plugins/manageSource/View.vue +697 -0
- package/src/plugins/manageSource/definition.ts +63 -0
- package/src/plugins/manageSource/index.ts +66 -0
- package/src/plugins/markdown/Preview.vue +77 -0
- package/src/plugins/markdown/View.vue +476 -0
- package/src/plugins/markdown/definition.ts +50 -0
- package/src/plugins/markdown/index.ts +36 -0
- package/src/plugins/presentHtml/Preview.vue +25 -0
- package/src/plugins/presentHtml/View.vue +52 -0
- package/src/plugins/presentHtml/definition.ts +27 -0
- package/src/plugins/presentHtml/helpers.ts +72 -0
- package/src/plugins/presentHtml/index.ts +41 -0
- package/src/plugins/presentMulmoScript/Preview.vue +23 -0
- package/src/plugins/presentMulmoScript/View.vue +1166 -0
- package/src/plugins/presentMulmoScript/definition.ts +95 -0
- package/src/plugins/presentMulmoScript/helpers.ts +162 -0
- package/src/plugins/presentMulmoScript/index.ts +40 -0
- package/src/plugins/scheduler/Preview.vue +67 -0
- package/src/plugins/scheduler/TasksTab.vue +205 -0
- package/src/plugins/scheduler/View.vue +565 -0
- package/src/plugins/scheduler/definition.ts +57 -0
- package/src/plugins/scheduler/index.ts +45 -0
- package/src/plugins/scheduler/viewModes.ts +26 -0
- package/src/plugins/spreadsheet/Preview.vue +29 -0
- package/src/plugins/spreadsheet/View.vue +997 -0
- package/src/plugins/spreadsheet/cellHighlights.ts +79 -0
- package/src/plugins/spreadsheet/definition.ts +121 -0
- package/src/plugins/spreadsheet/engine/calculator.ts +459 -0
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +81 -0
- package/src/plugins/spreadsheet/engine/date-parser.ts +220 -0
- package/src/plugins/spreadsheet/engine/date-utils.ts +56 -0
- package/src/plugins/spreadsheet/engine/engine.ts +176 -0
- package/src/plugins/spreadsheet/engine/evaluator.ts +390 -0
- package/src/plugins/spreadsheet/engine/formatter.ts +172 -0
- package/src/plugins/spreadsheet/engine/formulaRefs.ts +101 -0
- package/src/plugins/spreadsheet/engine/functions/date.ts +299 -0
- package/src/plugins/spreadsheet/engine/functions/financial.ts +387 -0
- package/src/plugins/spreadsheet/engine/functions/index.ts +16 -0
- package/src/plugins/spreadsheet/engine/functions/logical.ts +262 -0
- package/src/plugins/spreadsheet/engine/functions/lookup.ts +400 -0
- package/src/plugins/spreadsheet/engine/functions/mathematical.ts +297 -0
- package/src/plugins/spreadsheet/engine/functions/statistical.ts +338 -0
- package/src/plugins/spreadsheet/engine/functions/text.ts +389 -0
- package/src/plugins/spreadsheet/engine/index.ts +27 -0
- package/src/plugins/spreadsheet/engine/jsonCellLocator.ts +111 -0
- package/src/plugins/spreadsheet/engine/parser.ts +143 -0
- package/src/plugins/spreadsheet/engine/registry.ts +150 -0
- package/src/plugins/spreadsheet/engine/responseDecoder.ts +67 -0
- package/src/plugins/spreadsheet/engine/types.ts +64 -0
- package/src/plugins/spreadsheet/index.ts +36 -0
- package/src/plugins/textResponse/Preview.vue +94 -0
- package/src/plugins/textResponse/View.vue +503 -0
- package/src/plugins/textResponse/definition.ts +34 -0
- package/src/plugins/textResponse/index.ts +27 -0
- package/src/plugins/textResponse/plugin.ts +29 -0
- package/src/plugins/textResponse/samples.ts +97 -0
- package/src/plugins/textResponse/types.ts +11 -0
- package/src/plugins/todo/Preview.vue +63 -0
- package/src/plugins/todo/View.vue +364 -0
- package/src/plugins/todo/composables/useTodos.ts +177 -0
- package/src/plugins/todo/definition.ts +45 -0
- package/src/plugins/todo/index.ts +61 -0
- package/src/plugins/todo/labels.ts +163 -0
- package/src/plugins/todo/priority.ts +98 -0
- package/src/plugins/todo/viewModes.ts +19 -0
- package/src/plugins/ui-image/ImagePreview.vue +23 -0
- package/src/plugins/ui-image/ImageView.vue +34 -0
- package/src/plugins/ui-image/index.ts +3 -0
- package/src/plugins/ui-image/types.ts +4 -0
- package/src/plugins/wiki/Preview.vue +65 -0
- package/src/plugins/wiki/View.vue +342 -0
- package/src/plugins/wiki/definition.ts +25 -0
- package/src/plugins/wiki/helpers.ts +59 -0
- package/src/plugins/wiki/index.ts +52 -0
- package/src/router/guards.ts +61 -0
- package/src/router/index.ts +50 -0
- package/src/tools/index.ts +52 -0
- package/src/tools/types.ts +27 -0
- package/src/types/events.ts +16 -0
- package/src/types/fileTree.ts +13 -0
- package/src/types/notification.ts +67 -0
- package/src/types/session.ts +116 -0
- package/src/types/sse.ts +90 -0
- package/src/types/toolCallHistory.ts +13 -0
- package/src/utils/agent/eventDispatch.ts +74 -0
- package/src/utils/agent/request.ts +55 -0
- package/src/utils/agent/toolCalls.ts +62 -0
- package/src/utils/api.ts +218 -0
- package/src/utils/canvas/viewMode.ts +46 -0
- package/src/utils/dom/authTokenMeta.ts +20 -0
- package/src/utils/dom/clickOutside.ts +11 -0
- package/src/utils/dom/externalLink.ts +57 -0
- package/src/utils/dom/scrollable.ts +24 -0
- package/src/utils/errors.ts +11 -0
- package/src/utils/files/expandedDirs.ts +25 -0
- package/src/utils/files/filename.ts +12 -0
- package/src/utils/files/sortChildren.ts +20 -0
- package/src/utils/filesPreview/schedulerPreview.ts +38 -0
- package/src/utils/filesPreview/todoPreview.ts +40 -0
- package/src/utils/format/date.ts +85 -0
- package/src/utils/format/frontmatter.ts +80 -0
- package/src/utils/format/jsonSyntax.ts +109 -0
- package/src/utils/html/previewCsp.ts +65 -0
- package/src/utils/image/resolve.ts +8 -0
- package/src/utils/image/rewriteMarkdownImageRefs.ts +182 -0
- package/src/utils/markdown/extractFirstH1.ts +39 -0
- package/src/utils/notification/dispatch.ts +22 -0
- package/src/utils/path/relativeLink.ts +130 -0
- package/src/utils/role/icon.ts +20 -0
- package/src/utils/role/merge.ts +10 -0
- package/src/utils/role/plugins.ts +12 -0
- package/src/utils/session/mergeSessions.ts +103 -0
- package/src/utils/session/seedRoleDefault.ts +35 -0
- package/src/utils/session/sessionEntries.ts +121 -0
- package/src/utils/session/sessionFactory.ts +22 -0
- package/src/utils/session/sessionHelpers.ts +99 -0
- package/src/utils/tools/dedup.ts +17 -0
- package/src/utils/tools/mcp.ts +33 -0
- package/src/utils/tools/pendingCalls.ts +16 -0
- package/src/utils/tools/result.ts +40 -0
- package/src/utils/types.ts +44 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone MCP stdio server — spawned by the Claude CLI via --mcp-config.
|
|
3
|
+
* Bridges Claude's tool calls to our server endpoints and pushes ToolResults
|
|
4
|
+
* back to the active frontend SSE stream via the session registry.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ToolDefinition } from "gui-chat-protocol";
|
|
8
|
+
import { mcpTools, isMcpToolEnabled } from "./mcp-tools/index.js";
|
|
9
|
+
import { TOOL_ENDPOINTS, PLUGIN_DEFS } from "./plugin-names.js";
|
|
10
|
+
import { errorMessage } from "../utils/errors.js";
|
|
11
|
+
import { isNonEmptyString, isRecord } from "../utils/types.js";
|
|
12
|
+
import { API_ROUTES } from "../../src/config/apiRoutes.js";
|
|
13
|
+
import { env } from "../system/env.js";
|
|
14
|
+
import { extractFetchError } from "../utils/fetch.js";
|
|
15
|
+
import { safeResponseText } from "../utils/http.js";
|
|
16
|
+
import { readTextSafeSync } from "../utils/files/safe.js";
|
|
17
|
+
import { WORKSPACE_PATHS } from "../workspace/paths.js";
|
|
18
|
+
|
|
19
|
+
type JsonRpcId = string | number | null;
|
|
20
|
+
|
|
21
|
+
interface ToolCallParams {
|
|
22
|
+
name: string;
|
|
23
|
+
arguments?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface JsonRpcMessage {
|
|
27
|
+
jsonrpc: string;
|
|
28
|
+
id?: JsonRpcId;
|
|
29
|
+
method: string;
|
|
30
|
+
params?: ToolCallParams;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const isJsonRpcMessage = (v: unknown): v is JsonRpcMessage => isRecord(v) && "method" in v;
|
|
34
|
+
|
|
35
|
+
const SESSION_ID = env.mcpSessionId;
|
|
36
|
+
const PORT = env.port;
|
|
37
|
+
const PLUGIN_NAMES = env.mcpPluginNames;
|
|
38
|
+
const ROLE_IDS = env.mcpRoleIds;
|
|
39
|
+
const MCP_HOST = env.mcpHost;
|
|
40
|
+
const BASE_URL = `http://${MCP_HOST}:${PORT}`;
|
|
41
|
+
|
|
42
|
+
// Bearer token for /api/* calls back to the parent server (#272).
|
|
43
|
+
// The parent writes it to <workspace>/.session-token at startup; we
|
|
44
|
+
// read once at module load — the token is immutable for the server's
|
|
45
|
+
// lifetime. Same resolution order as bridges/cli/token.ts.
|
|
46
|
+
function readSessionToken(): string {
|
|
47
|
+
const fromEnv = process.env.MULMOCLAUDE_AUTH_TOKEN;
|
|
48
|
+
if (isNonEmptyString(fromEnv)) return fromEnv;
|
|
49
|
+
return readTextSafeSync(WORKSPACE_PATHS.sessionToken)?.trim() ?? "";
|
|
50
|
+
}
|
|
51
|
+
const SESSION_TOKEN = readSessionToken();
|
|
52
|
+
const AUTH_HEADER: Record<string, string> = SESSION_TOKEN ? { Authorization: `Bearer ${SESSION_TOKEN}` } : {};
|
|
53
|
+
|
|
54
|
+
interface ToolDef {
|
|
55
|
+
name: string;
|
|
56
|
+
description: string;
|
|
57
|
+
inputSchema: object;
|
|
58
|
+
endpoint?: string; // absent for tools handled specially (e.g. switchRole)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Combine `description` (one-liner) and `prompt` (detailed usage
|
|
62
|
+
// instructions) into the MCP tool description so Claude CLI sees
|
|
63
|
+
// both. The MCP protocol only has `description` — there's no
|
|
64
|
+
// `prompt` field — so the prompt content must ride along in the
|
|
65
|
+
// description string. The gui-chat-protocol ToolDefinition carries
|
|
66
|
+
// `prompt` separately because the Vue client uses it for different
|
|
67
|
+
// purposes, but the CLI needs it in-band.
|
|
68
|
+
function fromPackage(def: ToolDefinition, endpoint: string): ToolDef {
|
|
69
|
+
const parts = [def.description];
|
|
70
|
+
if (typeof def.prompt === "string" && def.prompt.length > 0) {
|
|
71
|
+
parts.push(def.prompt);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
name: def.name,
|
|
75
|
+
description: parts.join("\n\n"),
|
|
76
|
+
inputSchema: def.parameters ?? {},
|
|
77
|
+
endpoint,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Pure MCP tools (no GUI) — auto-registered from server/mcp-tools/
|
|
82
|
+
const mcpToolDefs: Record<string, ToolDef> = Object.fromEntries(
|
|
83
|
+
mcpTools.filter(isMcpToolEnabled).map((t) => [
|
|
84
|
+
t.definition.name,
|
|
85
|
+
{
|
|
86
|
+
name: t.definition.name,
|
|
87
|
+
description: t.definition.description,
|
|
88
|
+
inputSchema: t.definition.inputSchema,
|
|
89
|
+
},
|
|
90
|
+
]),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const ALL_TOOLS: Record<string, ToolDef> = {
|
|
94
|
+
...mcpToolDefs,
|
|
95
|
+
...Object.fromEntries(PLUGIN_DEFS.map((def) => [def.name, fromPackage(def, TOOL_ENDPOINTS[def.name])])),
|
|
96
|
+
switchRole: {
|
|
97
|
+
name: "switchRole",
|
|
98
|
+
description: "Switch to a different AI role, resetting the conversation context. Use when the user's request is better served by another role.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {
|
|
102
|
+
roleId: {
|
|
103
|
+
type: "string",
|
|
104
|
+
enum: ROLE_IDS,
|
|
105
|
+
description: "The ID of the role to switch to.",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
required: ["roleId"],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const tools = PLUGIN_NAMES.map((name) => ALL_TOOLS[name]).filter(Boolean);
|
|
114
|
+
|
|
115
|
+
function respond(msg: unknown): void {
|
|
116
|
+
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// All bridge calls go to the same backend on the same session, so
|
|
120
|
+
// every fetch was duplicating the same headers, method, and
|
|
121
|
+
// stringify boilerplate. `postJson` captures BASE_URL + SESSION_ID
|
|
122
|
+
// once and lets handleToolCall focus on what it's calling, not how.
|
|
123
|
+
//
|
|
124
|
+
// `path` is the absolute server path (e.g. /api/internal/tool-result)
|
|
125
|
+
// — the session query string is appended automatically.
|
|
126
|
+
//
|
|
127
|
+
// Both network errors and HTTP failures (4xx/5xx) are converted into
|
|
128
|
+
// a descriptive Error by default, so the outer catch in handleToolCall
|
|
129
|
+
// reports them as the failed tool call instead of a silent success.
|
|
130
|
+
// Pass `allowHttpError: true` for callers that want to inspect the
|
|
131
|
+
// response themselves (e.g. /api/mcp-tools/* which has its own
|
|
132
|
+
// status-aware result handling).
|
|
133
|
+
interface PostJsonOpts {
|
|
134
|
+
allowHttpError?: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function postJson(path: string, body: unknown, opts: PostJsonOpts = {}): Promise<Response> {
|
|
138
|
+
// SESSION_ID comes from the parent process env so it's effectively
|
|
139
|
+
// trusted, but encode it anyway — defense in depth against future
|
|
140
|
+
// callers passing unexpected characters (`&`, `#`, newlines, etc.).
|
|
141
|
+
// The path arg is used as-is because all current call sites pass
|
|
142
|
+
// hardcoded literals.
|
|
143
|
+
let res: Response;
|
|
144
|
+
try {
|
|
145
|
+
res = await fetch(`${BASE_URL}${path}?session=${encodeURIComponent(SESSION_ID)}`, {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: { "Content-Type": "application/json", ...AUTH_HEADER },
|
|
148
|
+
body: JSON.stringify(body),
|
|
149
|
+
});
|
|
150
|
+
} catch (err) {
|
|
151
|
+
throw new Error(`Network error calling ${path}: ${errorMessage(err)}`);
|
|
152
|
+
}
|
|
153
|
+
if (!opts.allowHttpError && !res.ok) {
|
|
154
|
+
const errBody = await safeResponseText(res, 500);
|
|
155
|
+
const detail = errBody ? `: ${errBody}` : "";
|
|
156
|
+
throw new Error(`HTTP ${res.status} calling ${path}${detail}`);
|
|
157
|
+
}
|
|
158
|
+
return res;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Bridge for the manageSkills tool. Routes by `action`:
|
|
162
|
+
// - "list" (default): GET /api/skills, push the list as a ToolResult
|
|
163
|
+
// - "save" : POST /api/skills with { name, description, body }
|
|
164
|
+
// - "delete" : DELETE /api/skills/:name
|
|
165
|
+
// In every case, after a successful mutation we re-fetch the list and
|
|
166
|
+
// push it so the canvas reflects the new state immediately.
|
|
167
|
+
async function handleManageSkills(args: Record<string, unknown>): Promise<string> {
|
|
168
|
+
const action = typeof args.action === "string" ? args.action : "list";
|
|
169
|
+
if (action === "save") return handleManageSkillsSave(args);
|
|
170
|
+
if (action === "update") return handleManageSkillsUpdate(args);
|
|
171
|
+
if (action === "delete") return handleManageSkillsDelete(args);
|
|
172
|
+
return handleManageSkillsList();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function fetchSkillsList(): Promise<{ name: string }[]> {
|
|
176
|
+
const url = `${BASE_URL}/api/skills?session=${encodeURIComponent(SESSION_ID)}`;
|
|
177
|
+
let res: Response;
|
|
178
|
+
try {
|
|
179
|
+
res = await fetch(url, { headers: AUTH_HEADER });
|
|
180
|
+
} catch (err) {
|
|
181
|
+
throw new Error(`Network error calling /api/skills: ${errorMessage(err)}`);
|
|
182
|
+
}
|
|
183
|
+
if (!res.ok) {
|
|
184
|
+
const body = await safeResponseText(res);
|
|
185
|
+
throw new Error(`HTTP ${res.status} calling /api/skills: ${body}`);
|
|
186
|
+
}
|
|
187
|
+
const body: { skills: { name: string }[] } = await res.json();
|
|
188
|
+
return body.skills;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function pushSkillsListResult(message: string): Promise<void> {
|
|
192
|
+
const skills = await fetchSkillsList();
|
|
193
|
+
await postJson(API_ROUTES.agent.internal.toolResult, {
|
|
194
|
+
toolName: "manageSkills",
|
|
195
|
+
uuid: crypto.randomUUID(),
|
|
196
|
+
title: "Skills",
|
|
197
|
+
message,
|
|
198
|
+
data: { skills },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function handleManageSkillsList(): Promise<string> {
|
|
203
|
+
const skills = await fetchSkillsList();
|
|
204
|
+
const suffix = skills.length === 1 ? "" : "s";
|
|
205
|
+
await postJson(API_ROUTES.agent.internal.toolResult, {
|
|
206
|
+
toolName: "manageSkills",
|
|
207
|
+
uuid: crypto.randomUUID(),
|
|
208
|
+
title: "Skills",
|
|
209
|
+
message: `Found ${skills.length} skill${suffix}.`,
|
|
210
|
+
data: { skills },
|
|
211
|
+
});
|
|
212
|
+
return `Listed ${skills.length} skill${suffix}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function handleManageSkillsSave(args: Record<string, unknown>): Promise<string> {
|
|
216
|
+
// Normalize name once up front so log / result messages below never
|
|
217
|
+
// interpolate an accidental object / number into `/${name}`.
|
|
218
|
+
const name = String(args.name ?? "");
|
|
219
|
+
const res = await postJson(
|
|
220
|
+
API_ROUTES.skills.create,
|
|
221
|
+
{
|
|
222
|
+
name,
|
|
223
|
+
description: args.description,
|
|
224
|
+
body: args.body,
|
|
225
|
+
},
|
|
226
|
+
{ allowHttpError: true },
|
|
227
|
+
);
|
|
228
|
+
if (!res.ok) {
|
|
229
|
+
return "Error: " + (await extractFetchError(res));
|
|
230
|
+
}
|
|
231
|
+
await pushSkillsListResult(`Saved skill "${name}".`);
|
|
232
|
+
return `Saved skill ${name}. Run with /${name}.`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function handleManageSkillsUpdate(args: Record<string, unknown>): Promise<string> {
|
|
236
|
+
const name = String(args.name ?? "");
|
|
237
|
+
const url = `${BASE_URL}/api/skills/${encodeURIComponent(name)}?session=${encodeURIComponent(SESSION_ID)}`;
|
|
238
|
+
let res: Response;
|
|
239
|
+
try {
|
|
240
|
+
res = await fetch(url, {
|
|
241
|
+
method: "PUT",
|
|
242
|
+
headers: { ...AUTH_HEADER, "Content-Type": "application/json" },
|
|
243
|
+
body: JSON.stringify({
|
|
244
|
+
description: args.description,
|
|
245
|
+
body: args.body,
|
|
246
|
+
}),
|
|
247
|
+
});
|
|
248
|
+
} catch (err) {
|
|
249
|
+
throw new Error(`Network error calling PUT /api/skills/${name}: ${errorMessage(err)}`);
|
|
250
|
+
}
|
|
251
|
+
if (!res.ok) {
|
|
252
|
+
return "Error: " + (await extractFetchError(res));
|
|
253
|
+
}
|
|
254
|
+
await pushSkillsListResult(`Updated skill "${name}".`);
|
|
255
|
+
return `Updated skill ${name}. The changes take effect in new sessions.`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function handleManageSkillsDelete(args: Record<string, unknown>): Promise<string> {
|
|
259
|
+
const name = String(args.name ?? "");
|
|
260
|
+
const url = `/api/skills/${encodeURIComponent(name)}?session=${encodeURIComponent(SESSION_ID)}`;
|
|
261
|
+
let res: Response;
|
|
262
|
+
try {
|
|
263
|
+
res = await fetch(`${BASE_URL}${url}`, {
|
|
264
|
+
method: "DELETE",
|
|
265
|
+
headers: AUTH_HEADER,
|
|
266
|
+
});
|
|
267
|
+
} catch (err) {
|
|
268
|
+
throw new Error(`Network error calling DELETE ${url}: ${errorMessage(err)}`);
|
|
269
|
+
}
|
|
270
|
+
if (!res.ok) {
|
|
271
|
+
return "Error: " + (await extractFetchError(res));
|
|
272
|
+
}
|
|
273
|
+
await pushSkillsListResult(`Deleted skill "${name}".`);
|
|
274
|
+
return `Deleted skill ${name}.`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function handleToolCall(name: string, args: Record<string, unknown>): Promise<string> {
|
|
278
|
+
if (name === "switchRole") {
|
|
279
|
+
await postJson(API_ROUTES.agent.internal.switchRole, {
|
|
280
|
+
roleId: args.roleId,
|
|
281
|
+
});
|
|
282
|
+
return `Switching to ${args.roleId} role`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (name === "manageRoles") {
|
|
286
|
+
const res = await postJson(API_ROUTES.roles.manage, args);
|
|
287
|
+
const result = await res.json();
|
|
288
|
+
|
|
289
|
+
// For the list action, push a visual canvas result so the viewer renders
|
|
290
|
+
if (args.action === "list" && result.success) {
|
|
291
|
+
await postJson(API_ROUTES.agent.internal.toolResult, {
|
|
292
|
+
toolName: "manageRoles",
|
|
293
|
+
uuid: crypto.randomUUID(),
|
|
294
|
+
...result,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return result.message ?? (result.error ? `Error: ${result.error}` : "Done");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (name === "manageSkills") return handleManageSkills(args);
|
|
302
|
+
|
|
303
|
+
// Pure MCP tools — call via /api/mcp-tools/:tool, return text directly
|
|
304
|
+
// (no frontend push). Opt out of postJson's HTTP error throw because
|
|
305
|
+
// we want to surface the JSON error body to the caller as a string.
|
|
306
|
+
const mcpTool = mcpTools.find((t) => t.definition.name === name);
|
|
307
|
+
if (mcpTool) {
|
|
308
|
+
const res = await postJson(`/api/mcp-tools/${name}`, args, {
|
|
309
|
+
allowHttpError: true,
|
|
310
|
+
});
|
|
311
|
+
const json = await res.json();
|
|
312
|
+
if (!res.ok) return `Error: ${json.error ?? res.status}`;
|
|
313
|
+
return typeof json.result === "string" ? json.result : JSON.stringify(json.result);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const tool = tools.find((t) => t.name === name);
|
|
317
|
+
if (!tool) throw new Error(`Unknown tool: ${name}`);
|
|
318
|
+
|
|
319
|
+
const res = await postJson(tool.endpoint!, args);
|
|
320
|
+
const result = await res.json();
|
|
321
|
+
|
|
322
|
+
// Push visual ToolResult to the frontend via the session
|
|
323
|
+
await postJson(API_ROUTES.agent.internal.toolResult, {
|
|
324
|
+
toolName: name,
|
|
325
|
+
uuid: crypto.randomUUID(),
|
|
326
|
+
...result,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const parts = [result.message, result.instructions].filter(Boolean);
|
|
330
|
+
return parts.length > 0 ? parts.join("\n") : "Done";
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let buffer = "";
|
|
334
|
+
|
|
335
|
+
process.stdin.on("data", (chunk: Buffer) => {
|
|
336
|
+
buffer += chunk.toString();
|
|
337
|
+
const lines = buffer.split("\n");
|
|
338
|
+
buffer = lines.pop() ?? "";
|
|
339
|
+
|
|
340
|
+
for (const line of lines) {
|
|
341
|
+
if (!line.trim()) continue;
|
|
342
|
+
let msg: unknown;
|
|
343
|
+
try {
|
|
344
|
+
msg = JSON.parse(line);
|
|
345
|
+
} catch {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (!isJsonRpcMessage(msg)) continue;
|
|
349
|
+
|
|
350
|
+
const { id, method, params } = msg;
|
|
351
|
+
|
|
352
|
+
if (method === "initialize") {
|
|
353
|
+
respond({
|
|
354
|
+
jsonrpc: "2.0",
|
|
355
|
+
id,
|
|
356
|
+
result: {
|
|
357
|
+
protocolVersion: "2024-11-05",
|
|
358
|
+
capabilities: { tools: {} },
|
|
359
|
+
serverInfo: { name: "mulmoclaude", version: "1.0.0" },
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
} else if (method === "tools/list") {
|
|
363
|
+
respond({
|
|
364
|
+
jsonrpc: "2.0",
|
|
365
|
+
id,
|
|
366
|
+
result: {
|
|
367
|
+
tools: tools.map((t) => ({
|
|
368
|
+
name: t.name,
|
|
369
|
+
description: t.description,
|
|
370
|
+
inputSchema: t.inputSchema,
|
|
371
|
+
})),
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
} else if (method === "tools/call") {
|
|
375
|
+
if (!params?.name) {
|
|
376
|
+
respond({
|
|
377
|
+
jsonrpc: "2.0",
|
|
378
|
+
id,
|
|
379
|
+
error: {
|
|
380
|
+
code: -32602,
|
|
381
|
+
message: "Invalid params: tools/call requires params.name",
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const toolArgs = params.arguments ?? {};
|
|
387
|
+
handleToolCall(params.name, toolArgs)
|
|
388
|
+
.then((text) => {
|
|
389
|
+
respond({
|
|
390
|
+
jsonrpc: "2.0",
|
|
391
|
+
id,
|
|
392
|
+
result: { content: [{ type: "text", text }] },
|
|
393
|
+
});
|
|
394
|
+
})
|
|
395
|
+
.catch((err: unknown) => {
|
|
396
|
+
respond({
|
|
397
|
+
jsonrpc: "2.0",
|
|
398
|
+
id,
|
|
399
|
+
result: {
|
|
400
|
+
content: [{ type: "text", text: String(err) }],
|
|
401
|
+
isError: true,
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
} else if (method === "ping") {
|
|
406
|
+
respond({ jsonrpc: "2.0", id, result: {} });
|
|
407
|
+
}
|
|
408
|
+
// notifications/initialized and other notifications: no response needed
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
process.stdin.on("end", () => process.exit(0));
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Router, Request, Response } from "express";
|
|
2
|
+
import { readXPost, searchX } from "./x.js";
|
|
3
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
4
|
+
import { notFound, sendError, serverError } from "../../utils/httpError.js";
|
|
5
|
+
import { API_ROUTES } from "../../../src/config/apiRoutes.js";
|
|
6
|
+
|
|
7
|
+
export interface McpTool {
|
|
8
|
+
definition: {
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
inputSchema: object;
|
|
12
|
+
};
|
|
13
|
+
requiredEnv?: string[];
|
|
14
|
+
prompt?: string;
|
|
15
|
+
handler: (args: Record<string, unknown>) => Promise<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const mcpTools: McpTool[] = [readXPost, searchX];
|
|
19
|
+
|
|
20
|
+
const toolMap = new Map(mcpTools.map((t) => [t.definition.name, t]));
|
|
21
|
+
|
|
22
|
+
export function isMcpToolEnabled(tool: McpTool): boolean {
|
|
23
|
+
return (tool.requiredEnv ?? []).every((key) => !!process.env[key]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Express router ──────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export const mcpToolsRouter = Router();
|
|
29
|
+
|
|
30
|
+
interface McpToolParams {
|
|
31
|
+
tool: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// GET /api/mcp-tools — returns { name, enabled, requiredEnv } for each tool (used by the role builder UI)
|
|
35
|
+
mcpToolsRouter.get(API_ROUTES.mcpTools.list, (_req: Request, res: Response) => {
|
|
36
|
+
res.json(
|
|
37
|
+
mcpTools.map((t) => ({
|
|
38
|
+
name: t.definition.name,
|
|
39
|
+
enabled: isMcpToolEnabled(t),
|
|
40
|
+
requiredEnv: t.requiredEnv ?? [],
|
|
41
|
+
prompt: t.prompt,
|
|
42
|
+
})),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// POST /api/mcp-tools/:tool — dispatches to the right handler
|
|
47
|
+
mcpToolsRouter.post(API_ROUTES.mcpTools.invoke, async (req: Request<McpToolParams, unknown, Record<string, unknown>>, res: Response) => {
|
|
48
|
+
const tool = toolMap.get(req.params.tool);
|
|
49
|
+
if (!tool) {
|
|
50
|
+
notFound(res, `Unknown MCP tool: ${req.params.tool}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!isMcpToolEnabled(tool)) {
|
|
54
|
+
sendError(res, 503, `Tool ${req.params.tool} is not configured.`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const result = await tool.handler(req.body);
|
|
59
|
+
res.json({ result });
|
|
60
|
+
} catch (err) {
|
|
61
|
+
serverError(res, errorMessage(err));
|
|
62
|
+
}
|
|
63
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { errorMessage } from "../../utils/errors.js";
|
|
2
|
+
import { safeResponseText } from "../../utils/http.js";
|
|
3
|
+
import { env } from "../../system/env.js";
|
|
4
|
+
|
|
5
|
+
const X_API_BASE = "https://api.twitter.com/2";
|
|
6
|
+
const TWEET_FIELDS = "tweet.fields=created_at,author_id,public_metrics,entities";
|
|
7
|
+
const EXPANSIONS = "expansions=author_id";
|
|
8
|
+
const USER_FIELDS = "user.fields=name,username";
|
|
9
|
+
|
|
10
|
+
interface XUser {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
username: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface XTweet {
|
|
17
|
+
id: string;
|
|
18
|
+
text: string;
|
|
19
|
+
author_id?: string;
|
|
20
|
+
created_at?: string;
|
|
21
|
+
public_metrics?: {
|
|
22
|
+
like_count: number;
|
|
23
|
+
retweet_count: number;
|
|
24
|
+
reply_count: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface XApiResponse {
|
|
29
|
+
data?: XTweet | XTweet[];
|
|
30
|
+
includes?: { users?: XUser[] };
|
|
31
|
+
errors?: { detail: string }[];
|
|
32
|
+
meta?: { result_count: number };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function fetchX(path: string): Promise<XApiResponse> {
|
|
36
|
+
const token = env.xBearerToken;
|
|
37
|
+
if (!token) throw new Error("X_BEARER_TOKEN is not configured in .env");
|
|
38
|
+
|
|
39
|
+
let response: Response;
|
|
40
|
+
try {
|
|
41
|
+
response = await fetch(`${X_API_BASE}${path}`, {
|
|
42
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
43
|
+
});
|
|
44
|
+
} catch (err) {
|
|
45
|
+
throw new Error(`Network error calling X API: ${errorMessage(err)}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (response.status === 401) throw new Error("X API error 401: Invalid or expired Bearer Token.");
|
|
49
|
+
if (response.status === 429) throw new Error("X API error 429: Rate limit exceeded. Please wait before retrying.");
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const body = await safeResponseText(response);
|
|
52
|
+
throw new Error(`X API error ${response.status}: ${body}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return response.json() as Promise<XApiResponse>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatTweet(tweet: XTweet, author?: XUser, url?: string): string {
|
|
59
|
+
const date = tweet.created_at ? new Date(tweet.created_at).toISOString().split("T")[0] : "";
|
|
60
|
+
const dateSuffix = date ? ` · ${date}` : "";
|
|
61
|
+
const byline = author ? `@${author.username} (${author.name})${dateSuffix}` : date;
|
|
62
|
+
const metrics = tweet.public_metrics
|
|
63
|
+
? `Likes: ${tweet.public_metrics.like_count} | Retweets: ${tweet.public_metrics.retweet_count} | Replies: ${tweet.public_metrics.reply_count}`
|
|
64
|
+
: "";
|
|
65
|
+
const link = url ?? "";
|
|
66
|
+
return [byline, "", tweet.text, "", metrics, link]
|
|
67
|
+
.filter((l) => l !== undefined)
|
|
68
|
+
.join("\n")
|
|
69
|
+
.trimEnd();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── readXPost ──────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export const readXPost = {
|
|
75
|
+
definition: {
|
|
76
|
+
name: "readXPost",
|
|
77
|
+
description: "Fetch the content of a single X (Twitter) post by URL or tweet ID. Returns the author, text, and engagement metrics.",
|
|
78
|
+
inputSchema: {
|
|
79
|
+
type: "object",
|
|
80
|
+
properties: {
|
|
81
|
+
url: {
|
|
82
|
+
type: "string",
|
|
83
|
+
description: "Full X post URL (https://x.com/user/status/ID) or bare tweet ID.",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
required: ["url"],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
requiredEnv: ["X_BEARER_TOKEN"],
|
|
91
|
+
|
|
92
|
+
prompt: "Use the readXPost tool whenever the user shares a URL from x.com or twitter.com.",
|
|
93
|
+
|
|
94
|
+
async handler(args: Record<string, unknown>): Promise<string> {
|
|
95
|
+
const url = String(args.url ?? "");
|
|
96
|
+
const match = url.match(/status\/(\d+)/);
|
|
97
|
+
const tweetId = match ? match[1] : /^\d+$/.test(url) ? url : null;
|
|
98
|
+
if (!tweetId) return `Could not extract a tweet ID from: ${url}. Provide a full x.com URL or a numeric tweet ID.`;
|
|
99
|
+
|
|
100
|
+
let data: XApiResponse;
|
|
101
|
+
try {
|
|
102
|
+
data = await fetchX(`/tweets/${tweetId}?${TWEET_FIELDS}&${EXPANSIONS}&${USER_FIELDS}`);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
return errorMessage(err);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (data.errors?.length) return `X API error: ${data.errors.map((e) => e.detail).join("; ")}`;
|
|
108
|
+
|
|
109
|
+
const tweet = data.data as XTweet | undefined;
|
|
110
|
+
if (!tweet) return "Tweet not found.";
|
|
111
|
+
|
|
112
|
+
const author = data.includes?.users?.find((u) => u.id === tweet.author_id);
|
|
113
|
+
const canonicalUrl = author ? `https://x.com/${author.username}/status/${tweet.id}` : undefined;
|
|
114
|
+
return formatTweet(tweet, author, canonicalUrl);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// ── searchX ───────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
export const searchX = {
|
|
121
|
+
definition: {
|
|
122
|
+
name: "searchX",
|
|
123
|
+
description: "Search recent X (Twitter) posts by keyword or query. Returns up to max_results posts (default 10, max 100).",
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: {
|
|
127
|
+
query: {
|
|
128
|
+
type: "string",
|
|
129
|
+
description: "X search query. Supports operators like from:user, #hashtag, -excludeword.",
|
|
130
|
+
},
|
|
131
|
+
max_results: {
|
|
132
|
+
type: "number",
|
|
133
|
+
description: "Number of results to return (10–100). Defaults to 10.",
|
|
134
|
+
},
|
|
135
|
+
sort_order: {
|
|
136
|
+
type: "string",
|
|
137
|
+
enum: ["recency", "relevancy"],
|
|
138
|
+
description: "'recency' = latest tweets first (default). 'relevancy' = most relevant (Top) first.",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
required: ["query"],
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
requiredEnv: ["X_BEARER_TOKEN"],
|
|
146
|
+
|
|
147
|
+
prompt: "Use the searchX tool to find recent posts on X by keyword or topic.",
|
|
148
|
+
|
|
149
|
+
async handler(args: Record<string, unknown>): Promise<string> {
|
|
150
|
+
const query = String(args.query ?? "").trim();
|
|
151
|
+
if (!query) return "A search query is required.";
|
|
152
|
+
|
|
153
|
+
const maxResults = Math.min(100, Math.max(10, Number(args.max_results ?? 10)));
|
|
154
|
+
|
|
155
|
+
let data: XApiResponse;
|
|
156
|
+
try {
|
|
157
|
+
const sortOrder = args.sort_order === "relevancy" ? "relevancy" : "recency";
|
|
158
|
+
const params = new URLSearchParams({
|
|
159
|
+
query,
|
|
160
|
+
max_results: String(maxResults),
|
|
161
|
+
sort_order: sortOrder,
|
|
162
|
+
});
|
|
163
|
+
params.append("tweet.fields", "created_at,author_id,public_metrics");
|
|
164
|
+
params.append("expansions", "author_id");
|
|
165
|
+
params.append("user.fields", "name,username");
|
|
166
|
+
data = await fetchX(`/tweets/search/recent?${params.toString()}`);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return errorMessage(err);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (data.errors?.length) return `X API error: ${data.errors.map((e) => e.detail).join("; ")}`;
|
|
172
|
+
|
|
173
|
+
const tweets = Array.isArray(data.data) ? data.data : [];
|
|
174
|
+
if (tweets.length === 0) return `No recent posts found for: "${query}"`;
|
|
175
|
+
|
|
176
|
+
const users = data.includes?.users ?? [];
|
|
177
|
+
const userMap = new Map(users.map((u) => [u.id, u]));
|
|
178
|
+
|
|
179
|
+
const lines: string[] = [`Search: "${query}" — ${tweets.length} result${tweets.length !== 1 ? "s" : ""}`, ""];
|
|
180
|
+
tweets.forEach((tweet, i) => {
|
|
181
|
+
const author = tweet.author_id ? userMap.get(tweet.author_id) : undefined;
|
|
182
|
+
lines.push(`${i + 1}. ${formatTweet(tweet, author)}`);
|
|
183
|
+
lines.push("");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return lines.join("\n").trimEnd();
|
|
187
|
+
},
|
|
188
|
+
};
|