@sentry/junior 0.272.0 → 0.273.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/dist/{adapter-BDYDi1F9.js → adapter-umap1cth.js} +1 -1
- package/dist/{admin-Dj7EFycG.js → admin-0o6E_3lo.js} +1 -1
- package/dist/{agent-runner-Cl6Usx2Y.js → agent-runner-D7Qz-lGO.js} +517 -298
- package/dist/api/schema.d.ts +1 -1
- package/dist/api/schema.js +4 -4
- package/dist/api.d.ts +1 -1
- package/dist/api.js +56 -573
- package/dist/{app-DjZblE2J.d.ts → app-BcyhJq8x.d.ts} +4 -9
- package/dist/app.d.ts +1 -1
- package/dist/app.js +673 -127
- package/dist/{assistant-reply-DN2w6JL_.js → assistant-reply-wZ7i4OFa.js} +1 -1
- package/dist/{automation-CV7g5h3A.js → automation-yffXcZt9.js} +1 -1
- package/dist/{briefs-PysAqnjV.js → briefs-CgMnlpIp.js} +4 -4
- package/dist/{catalog-runtime-BNeCWIaF.js → catalog-runtime-TygDsPOU.js} +1 -1
- package/dist/cli/chat.js +15 -15
- package/dist/cli/check.js +3 -3
- package/dist/cli/init.js +1 -1
- package/dist/cli/main.js +3 -3
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/snapshot-create.js +4 -4
- package/dist/cli/upgrade.d.ts +1 -1
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-FRphcDWW.js → client-DhqbV0Fe.js} +63 -5
- package/dist/{reporting-schema-CZREhYc-.js → common-Eu6sx09c.js} +4 -1
- package/dist/{config-BUP9wjiR.js → config-C6FTd7PX.js} +6 -14
- package/dist/{connections-CesHau9u.js → connections-Dq72KajR.js} +11 -11
- package/dist/{conversation-BtQzLoK8.js → conversation-T4oabbBi.js} +10 -0
- package/dist/{conversation-privacy-DiGvA9a3.js → conversation-privacy-BeRgrsph.js} +1 -1
- package/dist/{credential-sync-D2odJC30.js → credential-sync-BmCbHAp4.js} +15 -16
- package/dist/{db-BPLmOwXf.d.ts → db-DOzBGLn5.d.ts} +1 -1
- package/dist/{executor-DGDpyegI.js → executor-BafgvQcc.js} +2 -2
- package/dist/instrumentation.js +114 -3
- package/dist/{job-queue-CjB97sii.js → job-queue-C4lphW9Y.js} +3 -3
- package/dist/{logging-CjkDr-jP.js → logging-CNs1e04n.js} +6 -1
- package/dist/{message-D9y8PKA1.js → message-CETC5lsl.js} +9 -9
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-callback-server-02Pej6hj.js → oauth-callback-server-DcQZsKMm.js} +2 -2
- package/dist/{credentialed-CgTulRDH.js → projection-BOpi891B.js} +4576 -49
- package/dist/{prompt-vfceH0c-.js → prompt-B1ySjUyf.js} +1 -1
- package/dist/{registry-7XgwNgQe.js → registry-B-yUrv3h.js} +2 -2
- package/dist/{resolve-CuuxNfWG.js → resolve-DqVtd4Q9.js} +3 -3
- package/dist/{result-BRhb6iN1.js → result-DQwTiLTw.js} +9 -9
- package/dist/{runner-DaWCofuS.js → runner-Dco5qMq6.js} +11 -11
- package/dist/{sandbox-egress-signals-Bn6FA1UC.js → sandbox-egress-signals-DFc_9Mcv.js} +1 -1
- package/dist/{schema-D5VLF4NT.js → schema-5xHDGlBW.js} +9 -7
- package/dist/{schema-CNjAzYOw.d.ts → schema-9OGdsaxF.d.ts} +13 -0
- package/dist/{service-account-D9vrsi8d.js → service-account-CWfk9Cs-.js} +3 -3
- package/dist/{service-account-H45LIAG2.js → service-account-smdPFWNy.js} +3 -3
- package/dist/{session-stop-HKl0ECX0.js → session-stop-DaT_7tYa.js} +11 -11
- package/dist/{skills-CsiKu3-g.js → skills-BbtJSF2-.js} +2 -2
- package/dist/{spawn-DqjBxqWh.js → spawn-Ceikdjkp.js} +2 -2
- package/dist/{list-JZUP1yog.js → stop-dKUc-x2K.js} +996 -432
- package/dist/{store-DyVKH0QB.js → store-C1xuZcLd.js} +4 -4
- package/dist/{store-CPF2kzbu.js → store-DaxUuOmD.js} +3 -3
- package/dist/{task-queue-BbQXCcNX.js → task-queue-ymAh_-P1.js} +4 -4
- package/dist/{task-runner-ChzW5E7R.js → task-runner-vlH6pEm3.js} +6 -6
- package/dist/{thread-state-Ds8JTsX6.js → thread-state-BkUpE9Ae.js} +4 -5
- package/dist/{turn-failure-response-DCMvjsA0.js → turn-failure-response-DIsD7ip_.js} +7 -8
- package/dist/{turn-lifecycle-DM_77q33.js → turn-lifecycle-BdPKdPS8.js} +1 -1
- package/dist/turn-session-routing-BdCTDq9m.js +1625 -0
- package/dist/{validation-DcGelbBi.js → validation-BQjCfh84.js} +1 -1
- package/dist/{version-C48azPKq.js → version-BpiFGZZW.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-BCRuO8Wg.js → viewer-C6Y1aXjZ.js} +27 -15
- package/dist/{work-zxkmbJAp.js → work-ChPh_YEh.js} +10 -38
- package/dist/{worker-CNNxHF1V.js → worker-Be_sK_ou.js} +6 -6
- package/package.json +4 -4
- package/dist/turn-session-routing-BV_gX7n3.js +0 -6072
- package/dist/{oauth-relay-CTygMkNx.js → oauth-relay-C6ZAyJgf.js} +1 -1
package/dist/app.js
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { A as JUNIOR_PLUGIN_TASK_CALLBACK_ROUTE, E as withSpan, L as GOOGLE_IDENTITY_PROVIDER, N as JUNIOR_WORKSPACE_SNAPSHOT_JOB_CALLBACK_ROUTE, T as withLogContext, b as setSpanAttributes, h as logWarn, m as logInfo, p as logException, r as createRequestContext, w as withContext, z as normalizeIdentityEmail } from "./logging-
|
|
1
|
+
import { $i as runWithWorkspaceTeamId, Ai as findMatchingWatches, C as projectConversationMessages, Ca as createPluginLogger, Dn as getPluginApiRoutes, Dr as isVercelSandboxEgressRequest, E as settleConversationTitleWork, Ei as collectEventMatchKeys, Et as isInvalidConversationRecordError, Fa as readConversationEventPage, Fi as bindScheduledAutomationCredentialSubject, Fn as validatePlugins, Gi as getSlackClient, Gr as postSlackEphemeralMessage, Hn as dismissOpenReviewItems, Ii as bindSlackDirectCredentialSubject, Jn as searchReviewItems, Ka as listConversationAnnotations, Li as verifyEventAutomationCredentialSubject, Mr as resolveBaseUrl, Ni as takeDueTimerWatches, Nn as getPlugins, On as getPluginRoutes, Or as proxyVercelSandboxEgressRequest, Pi as bindEventAutomationCredentialSubject, Pn as setPlugins, Qi as getWorkspaceTeamId, Qn as canActOnReviewItem, Ri as verifyScheduledAutomationCredentialSubject, Rn as setBriefsConfig, Rr as slackMention, Sn as createUserTokenStore, Ua as resolveRootVisibility, Wr as openSlackModal, Xr as slackEntitySchema, Za as createPluginCache, Zr as slackExternalRefIdSchema, aa as setDashboardConversationLinkOptions, ao as setConfigDefaults, ar as reviewItemReaderForUser, b as appendConversationMessages, ba as createPluginState, bn as isExperimentalFeatureEnabled, ci as consumeSandboxEgressPermissionDeniedSignal, ct as CONVERSATION_WORK_STALE_ENQUEUE_MS, fn as SubscribedReplyReason, hn as getThreadStopDecision, ia as getDashboardTaskLink, io as getConfigDefaults, mi as normalizeSandboxEgressTracePropagationDomains, oi as clearSandboxEgressSignals, qi as isDmChannel, ra as getDashboardReviewItemLink, si as consumeSandboxEgressAuthRequiredSignal, ta as getDashboardConversationLink, ui as parseSandboxEgressCredentialToken, wa as readConversationAccessFromSql, wt as hasRunnableConversationWork, xa as createPluginEmbedder, xn as setExperimentalFeatures, yn as getExperimentalFeatures, zi as verifySlackDirectCredentialSubject } from "./projection-BOpi891B.js";
|
|
2
|
+
import { d as restoreRuntimeConfig, f as setBotModelConfig, g as snapshotRuntimeConfig, h as setSlackReactionConfig, i as getChatConfig, m as setRuntimeLimits, n as botConfig, o as getSlackBotToken, p as setProfiles, u as resetRuntimeConfig } from "./config-C6FTd7PX.js";
|
|
3
|
+
import { A as JUNIOR_PLUGIN_TASK_CALLBACK_ROUTE, E as withSpan, L as GOOGLE_IDENTITY_PROVIDER, N as JUNIOR_WORKSPACE_SNAPSHOT_JOB_CALLBACK_ROUTE, T as withLogContext, b as setSpanAttributes, h as logWarn, m as logInfo, p as logException, r as createRequestContext, w as withContext, z as normalizeIdentityEmail } from "./logging-CNs1e04n.js";
|
|
4
4
|
import { n as captureMessage } from "./sentry-MDkYAVPV.js";
|
|
5
|
-
import { t as JUNIOR_VERSION } from "./version-
|
|
5
|
+
import { t as JUNIOR_VERSION } from "./version-BpiFGZZW.js";
|
|
6
6
|
import { _ as slackMessageTsSchema, f as parseSlackTeamId, g as slackMessageTsFromMs, h as parseSlackMessageTs, m as slackChannelIdSchema, n as createSlackActor, p as parseSlackUserId, s as parseActorUserId, u as parseSlackChannelId } from "./actor-B4DYygOB.js";
|
|
7
|
-
import { C as getSqlExecutor,
|
|
7
|
+
import { C as getSqlExecutor, E as withConversationEventLock, N as resolveActorIdentityId, S as getDb, _ as closeDb, g as createCache, i as readActorIdentity, k as withConversationMutationLock, l as resolveViewerUser, p as parseAvatarUrl, x as getConversationStore, y as getConversationEventStore, z as recordGoogleSignIn } from "./viewer-C6Y1aXjZ.js";
|
|
8
8
|
import { a as juniorAgentInvocations, d as juniorDestinations, i as juniorAgentBindings, s as juniorConversations } from "./agent-invocations-C1RNOCs8.js";
|
|
9
|
-
import { _ as juniorConversationBriefs, b as juniorConversationEvents, c as juniorReviewItems, d as juniorAutomations, l as juniorAutomationRuns } from "./executor-
|
|
10
|
-
import { n as pluginCatalogRuntime } from "./catalog-runtime-
|
|
11
|
-
import { A as verifySlackSignature, B as scheduledAutomationRunAttributes, C as syncReviewItemSlackCards, D as parseSlackReviewItemFormTarget, E as parseSlackReviewItemClick, Et as ingestEvent, I as joinPublicChannel, O as parseSlackUserInputSubmission, S as readSlackReviewItemCardConversationId, T as buildSlackReviewItemModal, Tt as processWorkspaceSnapshotJob, X as readViewerAutomationSummary, _ as unpublishedArtifactHeaders, at as appendAndEnqueueWebMessage, et as effectiveTaskOutcomes, g as readPublicArtifact, h as publicArtifactHeaders, ht as requestAttachmentDeletion, j as parseSlackThreadId, k as runWithSlackInstallation, lt as recordWebConversationActivity, n as executeAgentRun, nt as deliverReviewItemChange, ot as conversationTurnIdForMessage, pt as collectAttachmentGarbage, rt as renderReviewItemMessage, t as createAgentRunner, ut as webActorFromEmail, v as lookupSlackUserProfile, vt as stopConversationSession, x as createSlackReviewItemShow, yt as recordSubagentEnded, z as logScheduledAutomationRunSkipped } from "./agent-runner-
|
|
12
|
-
import { At as slackEntitySchema, Bn as getDashboardTaskLink, Bt as clearSandboxEgressSignals, Cr as resolveRootVisibility, Dn as getSlackClient, Er as listConversationAnnotations, Fn as runWithWorkspaceTeamId, Ht as consumeSandboxEgressPermissionDeniedSignal, Ln as getDashboardConversationLink, M as canActOnReviewItem, Pn as getWorkspaceTeamId, S as dismissOpenReviewItems, Tt as postSlackEphemeralMessage, Vn as setDashboardConversationLinkOptions, Vt as consumeSandboxEgressAuthRequiredSignal, Wt as parseSandboxEgressCredentialToken, Xt as normalizeSandboxEgressTracePropagationDomains, _n as bindScheduledAutomationCredentialSubject, ar as createPluginLogger, bn as verifyScheduledAutomationCredentialSubject, c as getPluginRoutes, cn as collectEventMatchKeys, ct as proxyVercelSandboxEgressRequest, fn as findMatchingWatches, ft as resolveBaseUrl, gn as bindEventAutomationCredentialSubject, h as validatePlugins, hn as takeDueTimerWatches, jr as createPluginCache, jt as slackExternalRefIdSchema, kn as isDmChannel, m as setPlugins, n as createUserTokenStore, nr as createPluginState, or as readConversationAccessFromSql, p as getPlugins, rr as createPluginEmbedder, s as getPluginApiRoutes, st as isVercelSandboxEgressRequest, v as setBriefsConfig, vn as bindSlackDirectCredentialSubject, vt as slackMention, wt as openSlackModal, xn as verifySlackDirectCredentialSubject, yn as verifyEventAutomationCredentialSubject } from "./credentialed-CgTulRDH.js";
|
|
9
|
+
import { _ as juniorConversationBriefs, b as juniorConversationEvents, c as juniorReviewItems, d as juniorAutomations, l as juniorAutomationRuns } from "./executor-BafgvQcc.js";
|
|
10
|
+
import { n as pluginCatalogRuntime } from "./catalog-runtime-TygDsPOU.js";
|
|
11
|
+
import { A as verifySlackSignature, B as scheduledAutomationRunAttributes, C as syncReviewItemSlackCards, D as parseSlackReviewItemFormTarget, E as parseSlackReviewItemClick, Et as ingestEvent, I as joinPublicChannel, J as readViewerAutomationExecutions, K as ViewerTaskNotFoundError, O as parseSlackUserInputSubmission, Q as readViewerAutomations, S as readSlackReviewItemCardConversationId, T as buildSlackReviewItemModal, Tt as processWorkspaceSnapshotJob, X as readViewerAutomationSummary, _ as unpublishedArtifactHeaders, at as appendAndEnqueueWebMessage, et as effectiveTaskOutcomes, g as readPublicArtifact, h as publicArtifactHeaders, ht as requestAttachmentDeletion, j as parseSlackThreadId, k as runWithSlackInstallation, lt as recordWebConversationActivity, n as executeAgentRun, nt as deliverReviewItemChange, ot as conversationTurnIdForMessage, pt as collectAttachmentGarbage, rt as renderReviewItemMessage, t as createAgentRunner, ut as webActorFromEmail, v as lookupSlackUserProfile, vt as stopConversationSession, x as createSlackReviewItemShow, yt as recordSubagentEnded, z as logScheduledAutomationRunSkipped } from "./agent-runner-D7Qz-lGO.js";
|
|
13
12
|
import { a as withLock, i as fenceLock, n as MUTATION_LOCK_TTL_MS } from "./locks-D-0h111u.js";
|
|
14
|
-
import { r as getStateAdapter, t as disconnectStateAdapter } from "./adapter-
|
|
13
|
+
import { r as getStateAdapter, t as disconnectStateAdapter } from "./adapter-umap1cth.js";
|
|
15
14
|
import { n as getNextRunAtMs } from "./cadence-R7Em4RjE.js";
|
|
16
|
-
import { D as conversationSurfaceSchema, L as annotationCard, T as conversationReportStatusSchema, h as conversationFeedQuerySchema } from "./conversation-
|
|
17
|
-
import { b as listPendingAgentInvocationMailboxAppends, x as listPendingAgentInvocationResults } from "./store-
|
|
18
|
-
import { C as renderTaskInput, O as renderSlackObjectCard, d as getEventCatalog, f as SLACK_EVENT_NAMESPACE, g as slackChannelMessageRunsAutomation, h as slackChannelMessageReplyThreadTs, m as slackChannelMessageEvent } from "./session-stop-
|
|
15
|
+
import { D as conversationSurfaceSchema, L as annotationCard, T as conversationReportStatusSchema, Y as conversationBriefSchema, h as conversationFeedQuerySchema } from "./conversation-T4oabbBi.js";
|
|
16
|
+
import { b as listPendingAgentInvocationMailboxAppends, x as listPendingAgentInvocationResults } from "./store-C1xuZcLd.js";
|
|
17
|
+
import { C as renderTaskInput, O as renderSlackObjectCard, d as getEventCatalog, f as SLACK_EVENT_NAMESPACE, g as slackChannelMessageRunsAutomation, h as slackChannelMessageReplyThreadTs, m as slackChannelMessageEvent } from "./session-stop-DaT_7tYa.js";
|
|
19
18
|
import { h as buttonUserInputField } from "./reporting-window-DgHgwQB3.js";
|
|
20
|
-
import { c as setSandboxResourceConfig, o as getSandboxResourceConfig } from "./resolve-
|
|
21
|
-
import { a as getVercelConversationWorkQueue, d as getQueueProvider, g as isConversationQueueMessageRejectedError, h as conversationQueueMessageSchema, i as verifyWorkspaceSnapshotJobMessage, o as resolveConversationWorkQueueTopic, s as verifyConversationQueueMessage, t as WORKSPACE_SNAPSHOT_JOB_QUEUE_TOPIC } from "./job-queue-
|
|
22
|
-
import { S as removeActiveConversation, _ as hasRunnableConversationWork$1, c as clearExpiredConversationLease, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, r as appendAndEnqueueInboundMessage, v as listActiveConversationIds } from "./store-
|
|
23
|
-
import { M as
|
|
24
|
-
import {
|
|
19
|
+
import { c as setSandboxResourceConfig, o as getSandboxResourceConfig } from "./resolve-DqVtd4Q9.js";
|
|
20
|
+
import { a as getVercelConversationWorkQueue, d as getQueueProvider, g as isConversationQueueMessageRejectedError, h as conversationQueueMessageSchema, i as verifyWorkspaceSnapshotJobMessage, o as resolveConversationWorkQueueTopic, s as verifyConversationQueueMessage, t as WORKSPACE_SNAPSHOT_JOB_QUEUE_TOPIC } from "./job-queue-C4lphW9Y.js";
|
|
21
|
+
import { S as removeActiveConversation, _ as hasRunnableConversationWork$1, c as clearExpiredConversationLease, g as getConversationWorkState, h as getConversation, m as ensureConversationWake, r as appendAndEnqueueInboundMessage, v as listActiveConversationIds } from "./store-DaxUuOmD.js";
|
|
22
|
+
import { B as readScheduledAutomation, G as blockEventAutomation, H as saveScheduledAutomationInLock, K as collectEventAutomationMatchKeys, M as recordAutomationExecution, R as listScheduledAutomationsForTeam, S as existingConversationId, U as scheduledRunId, W as withScheduledAutomationLock, Y as findMatchingEventAutomations, a as confirmDispatchMailboxAppend, b as notifyAutomationBlocked, f as getPluginDispatchProjection, ht as eventGuidance, it as automationVersionOf, m as listPendingDispatchMailboxAppends, nt as eventAutomationRunLimitReason, o as createOrGetDispatch, p as isTerminalDispatchStatus, r as buildDispatchId, s as getDispatchConversationId, tt as admitEventAutomationRun, u as getDispatchRecord, v as markDispatchFailed } from "./turn-session-routing-BdCTDq9m.js";
|
|
23
|
+
import { M as scheduledRunSchema, l as automationListQuerySchema, s as automationExecutionStatusSchema } from "./automation-yffXcZt9.js";
|
|
24
|
+
import { n as validatePluginRegistrations, t as validatePluginEgressCredentialHooks } from "./validation-BQjCfh84.js";
|
|
25
25
|
import { a as pluginRuntimeRegistrationsFromPluginSet, n as pluginCatalogConfigFromEnv, r as pluginCatalogConfigFromPluginSet, t as defineJuniorPlugins } from "./plugins-DOm9Y6qI.js";
|
|
26
|
-
import { A as
|
|
27
|
-
import { n as enqueueAgentInvocationResult, o as getAuthPausedConversationTurnId } from "./result-
|
|
28
|
-
import { r as enqueueAgentInvocation } from "./work-
|
|
29
|
-
import { o as coerceThreadConversationState } from "./thread-state-
|
|
30
|
-
import { o as unlinkProvider } from "./connections-
|
|
31
|
-
import { a as GET$4, i as publishAppHomeView, r as GET$5, t as receiveLocalOAuthCredential } from "./credential-sync-
|
|
32
|
-
import { a as pluginTaskQueueConsumer, c as queueCallback, n as createVercelPluginTaskCallback, o as registerVercelPluginTaskDevConsumer } from "./task-queue-
|
|
33
|
-
import { t as processConversationWork } from "./worker-
|
|
34
|
-
import { t as bindMessageAgent } from "./message-
|
|
35
|
-
import { t as bindSpawnAgent } from "./spawn-
|
|
36
|
-
import { t as processPluginTask } from "./task-runner-
|
|
26
|
+
import { A as recordSkippedConversationMessage, B as startProcessingReaction, C as getProductionSlackAdapter, D as isAllowedSignInEmail, E as isAllowedDomain, F as normalizeIncomingSlackThreadId, G as closeReviewItemsAnsweredByReply, H as parseContent, I as withNormalizedThreadId, J as getAttachmentStorage, K as readReviewItemForUser, L as stopConversationTurn, M as clearSlackPendingReactions, N as isSlackMessageStopped, O as readEnvList, P as stopSlackThread, R as THREAD_OPTOUT_ACK, S as createProductionSlackWebhookServices, T as assertNoLegacyAllowedDomains, U as ReviewItemActionError, V as stripLeadingBotMention, W as actOnReviewItem, X as enqueueAgentDispatch, Z as GET$3, _ as archiveConversation, a as readConversationFeed, g as encodeConversationCursor, h as decodeConversationCursor, j as buildSlackInboundMessage, k as resolveAllowedDomains, n as readConversationDetail, o as readConversationRecordFromSql, q as reviewItemChange, t as stopConversationTurnForViewer, w as createSlackWebApiStatusSender, x as createProductionConversationWorkOptions, z as bindMessageAuthorIdentityId } from "./stop-dKUc-x2K.js";
|
|
27
|
+
import { n as enqueueAgentInvocationResult, o as getAuthPausedConversationTurnId } from "./result-DQwTiLTw.js";
|
|
28
|
+
import { r as enqueueAgentInvocation } from "./work-ChPh_YEh.js";
|
|
29
|
+
import { o as coerceThreadConversationState } from "./thread-state-BkUpE9Ae.js";
|
|
30
|
+
import { o as unlinkProvider } from "./connections-Dq72KajR.js";
|
|
31
|
+
import { a as GET$4, i as publishAppHomeView, r as GET$5, t as receiveLocalOAuthCredential } from "./credential-sync-BmCbHAp4.js";
|
|
32
|
+
import { a as pluginTaskQueueConsumer, c as queueCallback, n as createVercelPluginTaskCallback, o as registerVercelPluginTaskDevConsumer } from "./task-queue-ymAh_-P1.js";
|
|
33
|
+
import { t as processConversationWork } from "./worker-Be_sK_ou.js";
|
|
34
|
+
import { t as bindMessageAgent } from "./message-CETC5lsl.js";
|
|
35
|
+
import { t as bindSpawnAgent } from "./spawn-Ceikdjkp.js";
|
|
36
|
+
import { t as processPluginTask } from "./task-runner-vlH6pEm3.js";
|
|
37
37
|
import { createRequire } from "node:module";
|
|
38
38
|
import { Hono } from "hono";
|
|
39
|
-
import { z } from "zod";
|
|
40
39
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
40
|
+
import { z } from "zod";
|
|
41
41
|
import { and, asc, eq, inArray, isNotNull, isNull, lte, ne, or, sql } from "drizzle-orm";
|
|
42
42
|
import { EVENT_SUMMARY_MAX_LENGTH, EVENT_TEXT_MAX_LENGTH, dispatchOptionsSchema, eventSchema, eventTriggerFiltersText, isSlackDestination, userSchema } from "@sentry/junior-plugin-api";
|
|
43
43
|
import { pathToFileURL } from "node:url";
|
|
@@ -49,6 +49,7 @@ import * as acp from "@agentclientprotocol/sdk";
|
|
|
49
49
|
import dns from "node:dns";
|
|
50
50
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
51
51
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
52
|
+
import { HTTPException } from "hono/http-exception";
|
|
52
53
|
import { bodyLimit } from "hono/body-limit";
|
|
53
54
|
import { createAuthMiddleware } from "better-auth/api";
|
|
54
55
|
import { betterAuth } from "better-auth/minimal";
|
|
@@ -5287,59 +5288,6 @@ function redirectUriMatches(registered, requested) {
|
|
|
5287
5288
|
return candidate !== null && isLoopbackHttp(candidate) && candidate.hostname === url.hostname && candidate.pathname === url.pathname && candidate.search === url.search;
|
|
5288
5289
|
});
|
|
5289
5290
|
}
|
|
5290
|
-
// src/api/mcp/oauth/metadata.ts
|
|
5291
|
-
/**
|
|
5292
|
-
* Own the MCP OAuth paths, scope, and discovery documents.
|
|
5293
|
-
*
|
|
5294
|
-
* Every URL comes from the trusted public origin of the app. The documents
|
|
5295
|
-
* advertise Client ID Metadata Documents and no client registration endpoint.
|
|
5296
|
-
*/
|
|
5297
|
-
const MCP_PATH = "/api/mcp";
|
|
5298
|
-
const MCP_SCOPE = "conversations:read";
|
|
5299
|
-
const MCP_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource/api/mcp";
|
|
5300
|
-
const MCP_AUTHORIZATION_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
5301
|
-
const MCP_AUTHORIZE_PATH = "/api/mcp/oauth/authorize";
|
|
5302
|
-
const MCP_TOKEN_PATH = "/api/mcp/oauth/token";
|
|
5303
|
-
/** Dashboard-authenticated consent page for one pending authorization. */
|
|
5304
|
-
const MCP_CONSENT_PATH = "/_junior/mcp/authorize/:authorizationId";
|
|
5305
|
-
/** Resolve the MCP URLs for this request from the trusted public origin. */
|
|
5306
|
-
function resolveMcpUrls(request, baseURL) {
|
|
5307
|
-
const origin = resolvePublicOrigin(request, baseURL);
|
|
5308
|
-
if (!origin) throw new Error("Junior MCP requires a configured public base URL");
|
|
5309
|
-
const url = (path) => new URL(path, origin).toString();
|
|
5310
|
-
return {
|
|
5311
|
-
authorize: url(MCP_AUTHORIZE_PATH),
|
|
5312
|
-
issuer: origin.origin,
|
|
5313
|
-
resource: url(MCP_PATH),
|
|
5314
|
-
resourceMetadata: url(MCP_RESOURCE_METADATA_PATH),
|
|
5315
|
-
token: url(MCP_TOKEN_PATH),
|
|
5316
|
-
consent: (authorizationId) => url(MCP_CONSENT_PATH.replace(":authorizationId", encodeURIComponent(authorizationId)))
|
|
5317
|
-
};
|
|
5318
|
-
}
|
|
5319
|
-
/** RFC 9728 protected resource metadata for `/api/mcp`. */
|
|
5320
|
-
function protectedResourceMetadata(urls) {
|
|
5321
|
-
return {
|
|
5322
|
-
resource: urls.resource,
|
|
5323
|
-
authorization_servers: [urls.issuer],
|
|
5324
|
-
bearer_methods_supported: ["header"],
|
|
5325
|
-
scopes_supported: [MCP_SCOPE]
|
|
5326
|
-
};
|
|
5327
|
-
}
|
|
5328
|
-
/** RFC 8414 authorization server metadata. CIMD is the only client model. */
|
|
5329
|
-
function authorizationServerMetadata(urls) {
|
|
5330
|
-
return {
|
|
5331
|
-
issuer: urls.issuer,
|
|
5332
|
-
authorization_endpoint: urls.authorize,
|
|
5333
|
-
token_endpoint: urls.token,
|
|
5334
|
-
scopes_supported: [MCP_SCOPE],
|
|
5335
|
-
response_types_supported: ["code"],
|
|
5336
|
-
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
5337
|
-
code_challenge_methods_supported: ["S256"],
|
|
5338
|
-
token_endpoint_auth_methods_supported: ["none"],
|
|
5339
|
-
authorization_response_iss_parameter_supported: true,
|
|
5340
|
-
client_id_metadata_document_supported: true
|
|
5341
|
-
};
|
|
5342
|
-
}
|
|
5343
5291
|
// src/api/mcp/oauth/page.ts
|
|
5344
5292
|
/** Render one MCP sign-in page. It sends no referrer and loads no resources. */
|
|
5345
5293
|
function renderMcpPage(page) {
|
|
@@ -5355,6 +5303,42 @@ function renderMcpPage(page) {
|
|
|
5355
5303
|
title: page.title
|
|
5356
5304
|
});
|
|
5357
5305
|
}
|
|
5306
|
+
// src/api/mcp/oauth/scopes.ts
|
|
5307
|
+
/**
|
|
5308
|
+
* Own the MCP OAuth scopes. Each scope unlocks a fixed set of tools. No
|
|
5309
|
+
* scope lets a client send a message or start a Turn.
|
|
5310
|
+
*
|
|
5311
|
+
* A grant stores its scopes as one space-separated string (RFC 6749 section
|
|
5312
|
+
* 3.3), so grants from before the scope split still parse.
|
|
5313
|
+
*/
|
|
5314
|
+
/** Every scope, in the order that the consent page shows it. */
|
|
5315
|
+
const MCP_SCOPES = [
|
|
5316
|
+
"conversations:read",
|
|
5317
|
+
"conversations:manage",
|
|
5318
|
+
"review_items:read",
|
|
5319
|
+
"automations:read"
|
|
5320
|
+
];
|
|
5321
|
+
/** What each scope lets the client do, as the consent page says it. */
|
|
5322
|
+
const MCP_SCOPE_DESCRIPTIONS = {
|
|
5323
|
+
"conversations:read": "List and read the conversations that you started or take part in",
|
|
5324
|
+
"conversations:manage": "Archive your conversations and stop a running Turn",
|
|
5325
|
+
"review_items:read": "List and read your review items",
|
|
5326
|
+
"automations:read": "List and read the automations that you can see"
|
|
5327
|
+
};
|
|
5328
|
+
/** The `scope` value that names every scope. */
|
|
5329
|
+
const MCP_ALL_SCOPES = MCP_SCOPES.join(" ");
|
|
5330
|
+
/**
|
|
5331
|
+
* Parse a space-separated scope value into known scopes in canonical order.
|
|
5332
|
+
* Return `undefined` when it is empty or names an unknown scope.
|
|
5333
|
+
*/
|
|
5334
|
+
function parseMcpScopes(value) {
|
|
5335
|
+
const names = value.split(" ").filter(Boolean);
|
|
5336
|
+
if (names.length === 0) return void 0;
|
|
5337
|
+
if (!names.every((name) => MCP_SCOPES.includes(name))) return void 0;
|
|
5338
|
+
return MCP_SCOPES.filter((scope) => names.includes(scope));
|
|
5339
|
+
}
|
|
5340
|
+
/** A stored scope value: known scopes, space-separated, in canonical order. */
|
|
5341
|
+
const mcpScopeValueSchema = z.string().refine((value) => parseMcpScopes(value)?.join(" ") === value);
|
|
5358
5342
|
// src/api/mcp/oauth/authorization.ts
|
|
5359
5343
|
/**
|
|
5360
5344
|
* Own the OAuth authorization endpoint and its pending requests.
|
|
@@ -5376,7 +5360,7 @@ const mcpAuthorizationSchema = z.object({
|
|
|
5376
5360
|
expiresAtMs: z.number().int().positive(),
|
|
5377
5361
|
redirectUri: z.string().min(1),
|
|
5378
5362
|
resource: z.string().min(1),
|
|
5379
|
-
scope:
|
|
5363
|
+
scope: mcpScopeValueSchema,
|
|
5380
5364
|
state: z.string().max(STATE_MAX_LENGTH).optional()
|
|
5381
5365
|
}).strict();
|
|
5382
5366
|
/** The user who owns one consent page, and the hash of its form token. */
|
|
@@ -5483,8 +5467,8 @@ async function handleMcpAuthorize(args) {
|
|
|
5483
5467
|
const codeChallenge = params.get("code_challenge") ?? "";
|
|
5484
5468
|
if (params.get("code_challenge_method") !== "S256" || !CODE_CHALLENGE_PATTERN.test(codeChallenge)) return fail("invalid_request", "PKCE with S256 is required.");
|
|
5485
5469
|
if (oauthState !== void 0 && oauthState.length > STATE_MAX_LENGTH) return fail("invalid_request", "The state parameter is too long.");
|
|
5486
|
-
const
|
|
5487
|
-
if (
|
|
5470
|
+
const scopes = parseMcpScopes(params.get("scope")?.trim() || MCP_ALL_SCOPES);
|
|
5471
|
+
if (!scopes) return fail("invalid_scope", `Use only these scopes: ${MCP_ALL_SCOPES}.`);
|
|
5488
5472
|
if (params.get("resource") !== args.urls.resource) return fail("invalid_target", `The resource must be ${args.urls.resource}.`);
|
|
5489
5473
|
const authorizationId = randomBytes(32).toString("base64url");
|
|
5490
5474
|
const authorization = {
|
|
@@ -5494,7 +5478,7 @@ async function handleMcpAuthorize(args) {
|
|
|
5494
5478
|
expiresAtMs: Date.now() + AUTHORIZATION_TTL_MS,
|
|
5495
5479
|
redirectUri,
|
|
5496
5480
|
resource: args.urls.resource,
|
|
5497
|
-
scope:
|
|
5481
|
+
scope: scopes.join(" "),
|
|
5498
5482
|
state: oauthState
|
|
5499
5483
|
};
|
|
5500
5484
|
await args.state.set(authorizationKey(authorizationId), authorization, AUTHORIZATION_TTL_MS);
|
|
@@ -5529,7 +5513,7 @@ const authorizationCodeSchema = z.object({
|
|
|
5529
5513
|
grantId: z.string().uuid(),
|
|
5530
5514
|
redirectUri: z.string().min(1),
|
|
5531
5515
|
resource: z.string().min(1),
|
|
5532
|
-
scope:
|
|
5516
|
+
scope: mcpScopeValueSchema
|
|
5533
5517
|
}).strict();
|
|
5534
5518
|
const grantSchema = z.object({
|
|
5535
5519
|
clientId: z.string().min(1),
|
|
@@ -5537,7 +5521,7 @@ const grantSchema = z.object({
|
|
|
5537
5521
|
/** Absolute end of the grant. Refresh never moves it. */
|
|
5538
5522
|
expiresAtMs: z.number().int().positive(),
|
|
5539
5523
|
resource: z.string().min(1),
|
|
5540
|
-
scope:
|
|
5524
|
+
scope: mcpScopeValueSchema
|
|
5541
5525
|
}).strict();
|
|
5542
5526
|
const tokenSchema = z.object({ grantId: z.string().uuid() }).strict();
|
|
5543
5527
|
function codeKey(code) {
|
|
@@ -5572,7 +5556,7 @@ async function issueAuthorizationCode(args) {
|
|
|
5572
5556
|
grantId: randomUUID(),
|
|
5573
5557
|
redirectUri: args.redirectUri,
|
|
5574
5558
|
resource: args.resource,
|
|
5575
|
-
scope:
|
|
5559
|
+
scope: args.scope
|
|
5576
5560
|
};
|
|
5577
5561
|
await args.state.set(codeKey(code), record, CODE_TTL_MS);
|
|
5578
5562
|
return code;
|
|
@@ -5662,8 +5646,8 @@ async function refreshGrant(args) {
|
|
|
5662
5646
|
const token = await readRecord(args.state, key, tokenSchema);
|
|
5663
5647
|
const grant = token ? await readGrant(args.state, token.grantId) : void 0;
|
|
5664
5648
|
if (!token || !grant || grant.clientId !== args.clientId || args.resource !== void 0 && args.resource !== grant.resource) return invalidGrant("The refresh token is not valid.");
|
|
5665
|
-
if (args.scope !== void 0 && args.scope !== grant.scope) return {
|
|
5666
|
-
description: `
|
|
5649
|
+
if (args.scope !== void 0 && parseMcpScopes(args.scope)?.join(" ") !== grant.scope) return {
|
|
5650
|
+
description: `Refresh keeps the granted scope: ${grant.scope}.`,
|
|
5667
5651
|
error: "invalid_scope",
|
|
5668
5652
|
ok: false
|
|
5669
5653
|
};
|
|
@@ -5682,9 +5666,9 @@ async function refreshGrant(args) {
|
|
|
5682
5666
|
return await issueTokens(args.state, token.grantId, grant);
|
|
5683
5667
|
}
|
|
5684
5668
|
/**
|
|
5685
|
-
* Return the user email of one MCP access token for one
|
|
5686
|
-
* tokens and other bearer values never match, because
|
|
5687
|
-
* stores MCP access tokens.
|
|
5669
|
+
* Return the user email and scopes of one MCP access token for one
|
|
5670
|
+
* resource. Personal tokens and other bearer values never match, because
|
|
5671
|
+
* only this module stores MCP access tokens.
|
|
5688
5672
|
*/
|
|
5689
5673
|
async function authenticateMcpAccessToken(args) {
|
|
5690
5674
|
if (!args.token.startsWith(ACCESS_TOKEN_PREFIX)) return void 0;
|
|
@@ -5692,7 +5676,62 @@ async function authenticateMcpAccessToken(args) {
|
|
|
5692
5676
|
if (!token) return void 0;
|
|
5693
5677
|
const grant = await readGrant(args.state, token.grantId);
|
|
5694
5678
|
if (!grant || grant.resource !== args.resource) return void 0;
|
|
5695
|
-
return
|
|
5679
|
+
return {
|
|
5680
|
+
email: grant.email,
|
|
5681
|
+
scopes: parseMcpScopes(grant.scope) ?? []
|
|
5682
|
+
};
|
|
5683
|
+
}
|
|
5684
|
+
// src/api/mcp/oauth/metadata.ts
|
|
5685
|
+
/**
|
|
5686
|
+
* Own the MCP OAuth paths and discovery documents.
|
|
5687
|
+
*
|
|
5688
|
+
* Every URL comes from the trusted public origin of the app. The documents
|
|
5689
|
+
* advertise Client ID Metadata Documents and no client registration endpoint.
|
|
5690
|
+
*/
|
|
5691
|
+
const MCP_PATH = "/api/mcp";
|
|
5692
|
+
const MCP_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource/api/mcp";
|
|
5693
|
+
const MCP_AUTHORIZATION_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
5694
|
+
const MCP_AUTHORIZE_PATH = "/api/mcp/oauth/authorize";
|
|
5695
|
+
const MCP_TOKEN_PATH = "/api/mcp/oauth/token";
|
|
5696
|
+
/** Dashboard-authenticated consent page for one pending authorization. */
|
|
5697
|
+
const MCP_CONSENT_PATH = "/_junior/mcp/authorize/:authorizationId";
|
|
5698
|
+
/** Resolve the MCP URLs for this request from the trusted public origin. */
|
|
5699
|
+
function resolveMcpUrls(request, baseURL) {
|
|
5700
|
+
const origin = resolvePublicOrigin(request, baseURL);
|
|
5701
|
+
if (!origin) throw new Error("Junior MCP requires a configured public base URL");
|
|
5702
|
+
const url = (path) => new URL(path, origin).toString();
|
|
5703
|
+
return {
|
|
5704
|
+
authorize: url(MCP_AUTHORIZE_PATH),
|
|
5705
|
+
issuer: origin.origin,
|
|
5706
|
+
resource: url(MCP_PATH),
|
|
5707
|
+
resourceMetadata: url(MCP_RESOURCE_METADATA_PATH),
|
|
5708
|
+
token: url(MCP_TOKEN_PATH),
|
|
5709
|
+
consent: (authorizationId) => url(MCP_CONSENT_PATH.replace(":authorizationId", encodeURIComponent(authorizationId)))
|
|
5710
|
+
};
|
|
5711
|
+
}
|
|
5712
|
+
/** RFC 9728 protected resource metadata for `/api/mcp`. */
|
|
5713
|
+
function protectedResourceMetadata(urls) {
|
|
5714
|
+
return {
|
|
5715
|
+
resource: urls.resource,
|
|
5716
|
+
authorization_servers: [urls.issuer],
|
|
5717
|
+
bearer_methods_supported: ["header"],
|
|
5718
|
+
scopes_supported: [...MCP_SCOPES]
|
|
5719
|
+
};
|
|
5720
|
+
}
|
|
5721
|
+
/** RFC 8414 authorization server metadata. CIMD is the only client model. */
|
|
5722
|
+
function authorizationServerMetadata(urls) {
|
|
5723
|
+
return {
|
|
5724
|
+
issuer: urls.issuer,
|
|
5725
|
+
authorization_endpoint: urls.authorize,
|
|
5726
|
+
token_endpoint: urls.token,
|
|
5727
|
+
scopes_supported: [...MCP_SCOPES],
|
|
5728
|
+
response_types_supported: ["code"],
|
|
5729
|
+
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
5730
|
+
code_challenge_methods_supported: ["S256"],
|
|
5731
|
+
token_endpoint_auth_methods_supported: ["none"],
|
|
5732
|
+
authorization_response_iss_parameter_supported: true,
|
|
5733
|
+
client_id_metadata_document_supported: true
|
|
5734
|
+
};
|
|
5696
5735
|
}
|
|
5697
5736
|
// src/api/mcp/oauth/consent-page.ts
|
|
5698
5737
|
/**
|
|
@@ -5720,8 +5759,10 @@ function consentPage(args) {
|
|
|
5720
5759
|
const clientHost = new URL(args.authorization.clientId).host;
|
|
5721
5760
|
const clientName = args.authorization.clientName ?? clientHost;
|
|
5722
5761
|
const button = (action, label, primary) => `<button type="submit" name="action" value="${action}" style="border:0;border-radius:.375rem;cursor:pointer;font:inherit;font-weight:700;padding:.625rem 1rem;margin-right:.5rem;${primary ? "background:#a78bfa;color:#000" : "background:#333;color:#fff"}">${label}</button>`;
|
|
5762
|
+
const scopeItems = (parseMcpScopes(args.authorization.scope) ?? []).map((scope) => `<li style="margin:0 0 .25rem">${escapeHtml(MCP_SCOPE_DESCRIPTIONS[scope])} <code style="color:#8a8a8a">${escapeHtml(scope)}</code></li>`).join("");
|
|
5723
5763
|
return renderMcpPage({
|
|
5724
|
-
bodyHtml: ` <p style="margin:0 0
|
|
5764
|
+
bodyHtml: ` <p style="margin:0 0 .5rem;color:#b8b8b8;line-height:1.5">The app is published at <strong style="color:#fff">${escapeHtml(clientHost)}</strong>. In ${escapeHtml(args.agentName)}, it can:</p>
|
|
5765
|
+
<ul style="margin:0 0 1rem;padding-left:1.25rem;color:#b8b8b8;line-height:1.5">${scopeItems}</ul>
|
|
5725
5766
|
<form method="post">
|
|
5726
5767
|
<input type="hidden" name="csrf" value="${escapeHtml(args.csrfToken)}">
|
|
5727
5768
|
${button("approve", "Allow", true)}${button("deny", "Deny", false)}
|
|
@@ -5791,13 +5832,15 @@ async function handleMcpConsentPage(args) {
|
|
|
5791
5832
|
email: args.user.email,
|
|
5792
5833
|
redirectUri: authorization.redirectUri,
|
|
5793
5834
|
resource: authorization.resource,
|
|
5835
|
+
scope: authorization.scope,
|
|
5794
5836
|
state: args.state
|
|
5795
5837
|
}) },
|
|
5796
5838
|
redirectUri: authorization.redirectUri,
|
|
5797
5839
|
state: authorization.state
|
|
5798
5840
|
});
|
|
5799
5841
|
}
|
|
5800
|
-
// src/api/mcp/
|
|
5842
|
+
// src/api/mcp/conversation.ts
|
|
5843
|
+
/** Own the MCP view of one Conversation, shared by the conversation tools. */
|
|
5801
5844
|
/** MCP view of one Conversation. It omits dashboard-only fields. */
|
|
5802
5845
|
const mcpConversationSchema = z.object({
|
|
5803
5846
|
conversationId: z.string(),
|
|
@@ -5811,6 +5854,7 @@ const mcpConversationSchema = z.object({
|
|
|
5811
5854
|
archivedAt: z.string().optional(),
|
|
5812
5855
|
url: z.string().optional()
|
|
5813
5856
|
}).strict();
|
|
5857
|
+
/** Map a Conversation summary to its MCP view. */
|
|
5814
5858
|
function mcpConversation(summary) {
|
|
5815
5859
|
return {
|
|
5816
5860
|
conversationId: summary.conversationId,
|
|
@@ -5825,34 +5869,519 @@ function mcpConversation(summary) {
|
|
|
5825
5869
|
url: getDashboardConversationLink(summary.conversationId)
|
|
5826
5870
|
};
|
|
5827
5871
|
}
|
|
5828
|
-
/**
|
|
5829
|
-
|
|
5872
|
+
/** Conversation id param shared by the conversation tools. */
|
|
5873
|
+
const mcpConversationIdSchema = z.string().min(1).describe("Conversation id from searchConversations.");
|
|
5874
|
+
/** MCP view of one user or assistant Message. */
|
|
5875
|
+
const mcpMessageSchema = z.object({
|
|
5876
|
+
seq: z.number().int(),
|
|
5877
|
+
createdAt: z.string(),
|
|
5878
|
+
role: z.enum(["assistant", "user"]),
|
|
5879
|
+
author: z.string().optional(),
|
|
5880
|
+
text: z.string(),
|
|
5881
|
+
attachments: z.array(z.object({
|
|
5882
|
+
filename: z.string(),
|
|
5883
|
+
contentType: z.string(),
|
|
5884
|
+
bytes: z.number().int()
|
|
5885
|
+
}).strict()).optional()
|
|
5886
|
+
}).strict();
|
|
5887
|
+
/**
|
|
5888
|
+
* Keep the user and assistant Messages of a report event page, in order.
|
|
5889
|
+
* System Messages and Messages with no text or attachment are left out.
|
|
5890
|
+
*/
|
|
5891
|
+
function mcpMessages(events) {
|
|
5892
|
+
return events.flatMap((event) => {
|
|
5893
|
+
const data = event.data;
|
|
5894
|
+
if (data.type !== "message" || data.role === "system") return [];
|
|
5895
|
+
const text = data.text ?? "";
|
|
5896
|
+
const attachments = data.attachments?.map(({ bytes, contentType, filename }) => ({
|
|
5897
|
+
bytes,
|
|
5898
|
+
contentType,
|
|
5899
|
+
filename
|
|
5900
|
+
}));
|
|
5901
|
+
if (!text && !attachments?.length) return [];
|
|
5902
|
+
const author = data.actorIdentity?.fullName ?? data.actorIdentity?.slackUserName;
|
|
5903
|
+
return [{
|
|
5904
|
+
seq: event.seq,
|
|
5905
|
+
createdAt: event.createdAt,
|
|
5906
|
+
role: data.role,
|
|
5907
|
+
...author ? { author } : void 0,
|
|
5908
|
+
text,
|
|
5909
|
+
...attachments?.length ? { attachments } : void 0
|
|
5910
|
+
}];
|
|
5911
|
+
});
|
|
5912
|
+
}
|
|
5913
|
+
// src/api/mcp/tool-context.ts
|
|
5914
|
+
/**
|
|
5915
|
+
* Return one structured tool result. The text block holds the same JSON, as
|
|
5916
|
+
* the MCP spec recommends for clients that do not read `structuredContent`.
|
|
5917
|
+
*/
|
|
5918
|
+
function mcpToolResult(result) {
|
|
5919
|
+
return {
|
|
5920
|
+
content: [{
|
|
5921
|
+
type: "text",
|
|
5922
|
+
text: JSON.stringify(result)
|
|
5923
|
+
}],
|
|
5924
|
+
structuredContent: result
|
|
5925
|
+
};
|
|
5926
|
+
}
|
|
5927
|
+
/** Return a tool error that the client model can read and repair. */
|
|
5928
|
+
function mcpToolError(message) {
|
|
5929
|
+
return {
|
|
5930
|
+
content: [{
|
|
5931
|
+
type: "text",
|
|
5932
|
+
text: message
|
|
5933
|
+
}],
|
|
5934
|
+
isError: true
|
|
5935
|
+
};
|
|
5936
|
+
}
|
|
5937
|
+
/**
|
|
5938
|
+
* Turn a client error from a shared API function, such as a `404` or a
|
|
5939
|
+
* `409`, into a tool error. Rethrow every other failure.
|
|
5940
|
+
*/
|
|
5941
|
+
function apiToolError(error) {
|
|
5942
|
+
if (error instanceof HTTPException && error.status < 500) return mcpToolError(error.message);
|
|
5943
|
+
throw error;
|
|
5944
|
+
}
|
|
5945
|
+
/**
|
|
5946
|
+
* Return whether `user` started or takes part in a Conversation. MCP tools
|
|
5947
|
+
* read and change only these Conversations, the same set that
|
|
5948
|
+
* `searchConversations` shows.
|
|
5949
|
+
*/
|
|
5950
|
+
async function isConversationParticipant(user, conversationId) {
|
|
5951
|
+
return (await readConversationAccessFromSql(getDb(), [conversationId], user)).get(conversationId)?.isParticipant ?? false;
|
|
5952
|
+
}
|
|
5953
|
+
// src/api/mcp/tools/archive-conversation.ts
|
|
5954
|
+
/** Register `archiveConversation`, which archives or restores for the user. */
|
|
5955
|
+
function registerArchiveConversationTool(server, { user }) {
|
|
5956
|
+
server.registerTool("archiveConversation", {
|
|
5957
|
+
title: "Archive conversation",
|
|
5958
|
+
description: "Archive one of your conversations, or restore it. Archiving hides it only for you, and new activity restores it.",
|
|
5959
|
+
inputSchema: {
|
|
5960
|
+
conversationId: mcpConversationIdSchema,
|
|
5961
|
+
archived: z.boolean().default(true).describe("False restores an archived conversation."),
|
|
5962
|
+
lastSeenAt: z.string().datetime().describe("`lastSeenAt` from searchConversations or getConversation. The archive fails when the conversation has newer activity.")
|
|
5963
|
+
},
|
|
5964
|
+
outputSchema: { archivedAt: z.string().nullable() },
|
|
5965
|
+
annotations: {
|
|
5966
|
+
destructiveHint: false,
|
|
5967
|
+
idempotentHint: true,
|
|
5968
|
+
openWorldHint: false,
|
|
5969
|
+
readOnlyHint: false
|
|
5970
|
+
}
|
|
5971
|
+
}, async ({ archived, conversationId, lastSeenAt }) => {
|
|
5972
|
+
try {
|
|
5973
|
+
return mcpToolResult(await archiveConversation(user, conversationId, {
|
|
5974
|
+
archived,
|
|
5975
|
+
lastSeenAt
|
|
5976
|
+
}));
|
|
5977
|
+
} catch (error) {
|
|
5978
|
+
return apiToolError(error);
|
|
5979
|
+
}
|
|
5980
|
+
});
|
|
5981
|
+
}
|
|
5982
|
+
// src/api/mcp/automation.ts
|
|
5983
|
+
/** Own the MCP view of one Automation, shared by the automation tools. */
|
|
5984
|
+
/** MCP view of one Automation. It omits cost and run-count charts. */
|
|
5985
|
+
const mcpAutomationSchema = z.object({
|
|
5986
|
+
id: z.string(),
|
|
5987
|
+
kind: z.enum(["scheduled", "event"]),
|
|
5988
|
+
title: z.string(),
|
|
5989
|
+
instruction: z.string(),
|
|
5990
|
+
status: z.enum([
|
|
5991
|
+
"active",
|
|
5992
|
+
"blocked",
|
|
5993
|
+
"paused",
|
|
5994
|
+
"completed"
|
|
5995
|
+
]),
|
|
5996
|
+
statusReason: z.string().optional(),
|
|
5997
|
+
trigger: z.string().describe("The schedule, or the events and filters that start a run."),
|
|
5998
|
+
nextRunAt: z.string().optional(),
|
|
5999
|
+
destination: z.string().describe("Slack channel that gets the results."),
|
|
6000
|
+
createdBy: z.string(),
|
|
6001
|
+
ownedByYou: z.boolean(),
|
|
6002
|
+
lastRunAt: z.string().optional(),
|
|
6003
|
+
lastRunStatus: z.enum([
|
|
6004
|
+
"blocked",
|
|
6005
|
+
"completed",
|
|
6006
|
+
"failed"
|
|
6007
|
+
]).optional(),
|
|
6008
|
+
lastConversationId: z.string().optional(),
|
|
6009
|
+
url: z.string().optional()
|
|
6010
|
+
}).strict();
|
|
6011
|
+
function triggerText(automation) {
|
|
6012
|
+
if (automation.kind === "scheduled") return `${automation.schedule} (${automation.timezone})`;
|
|
6013
|
+
const { events, filters } = automation.trigger;
|
|
6014
|
+
return [`${events.join(", ")} on ${automation.resource}`, ...filters].join("; ");
|
|
6015
|
+
}
|
|
6016
|
+
/** Map an Automation summary to its MCP view. */
|
|
6017
|
+
function mcpAutomation(automation) {
|
|
6018
|
+
const url = getDashboardTaskLink(automation.id);
|
|
6019
|
+
return {
|
|
6020
|
+
id: automation.id,
|
|
6021
|
+
kind: automation.kind,
|
|
6022
|
+
title: automation.title,
|
|
6023
|
+
instruction: automation.instruction,
|
|
6024
|
+
status: automation.status,
|
|
6025
|
+
...automation.statusReason ? { statusReason: automation.statusReason } : void 0,
|
|
6026
|
+
trigger: triggerText(automation),
|
|
6027
|
+
...automation.kind === "scheduled" && automation.nextRunAt ? { nextRunAt: automation.nextRunAt } : void 0,
|
|
6028
|
+
destination: automation.destination.label,
|
|
6029
|
+
createdBy: automation.createdBy,
|
|
6030
|
+
ownedByYou: automation.ownedByViewer,
|
|
6031
|
+
...automation.lastRunAt ? { lastRunAt: automation.lastRunAt } : void 0,
|
|
6032
|
+
...automation.lastRunStatus ? { lastRunStatus: automation.lastRunStatus } : void 0,
|
|
6033
|
+
...automation.lastConversationId ? { lastConversationId: automation.lastConversationId } : void 0,
|
|
6034
|
+
...url ? { url } : void 0
|
|
6035
|
+
};
|
|
6036
|
+
}
|
|
6037
|
+
// src/api/mcp/tools/get-automation.ts
|
|
6038
|
+
const RECENT_RUN_LIMIT = 10;
|
|
6039
|
+
/** Register `getAutomation`, which reads one Automation and its recent runs. */
|
|
6040
|
+
function registerGetAutomationTool(server, { user }) {
|
|
6041
|
+
server.registerTool("getAutomation", {
|
|
6042
|
+
title: "Get automation",
|
|
6043
|
+
description: `Get one automation and its ${RECENT_RUN_LIMIT} newest runs.`,
|
|
6044
|
+
inputSchema: { id: z.string().min(1).describe("Automation id from searchAutomations.") },
|
|
6045
|
+
outputSchema: {
|
|
6046
|
+
automation: mcpAutomationSchema,
|
|
6047
|
+
runs: z.array(z.object({
|
|
6048
|
+
executedAt: z.string(),
|
|
6049
|
+
status: automationExecutionStatusSchema,
|
|
6050
|
+
error: z.string().optional(),
|
|
6051
|
+
conversationId: z.string().optional()
|
|
6052
|
+
}).strict())
|
|
6053
|
+
},
|
|
6054
|
+
annotations: {
|
|
6055
|
+
destructiveHint: false,
|
|
6056
|
+
idempotentHint: true,
|
|
6057
|
+
openWorldHint: false,
|
|
6058
|
+
readOnlyHint: true
|
|
6059
|
+
}
|
|
6060
|
+
}, async ({ id }) => {
|
|
6061
|
+
try {
|
|
6062
|
+
const list = await readViewerAutomationExecutions(user, id);
|
|
6063
|
+
return mcpToolResult({
|
|
6064
|
+
automation: mcpAutomation(list.automation),
|
|
6065
|
+
runs: list.executions.slice(0, RECENT_RUN_LIMIT).map(({ conversationId, error, executedAt, status }) => ({
|
|
6066
|
+
executedAt,
|
|
6067
|
+
status,
|
|
6068
|
+
...error ? { error } : void 0,
|
|
6069
|
+
...conversationId ? { conversationId } : void 0
|
|
6070
|
+
}))
|
|
6071
|
+
});
|
|
6072
|
+
} catch (error) {
|
|
6073
|
+
if (error instanceof ViewerTaskNotFoundError) return mcpToolError("Automation not found.");
|
|
6074
|
+
throw error;
|
|
6075
|
+
}
|
|
6076
|
+
});
|
|
6077
|
+
}
|
|
6078
|
+
// src/api/mcp/tools/get-conversation.ts
|
|
6079
|
+
const mcpBriefSchema = conversationBriefSchema.omit({
|
|
6080
|
+
keywords: true,
|
|
6081
|
+
record: true,
|
|
6082
|
+
schemaVersion: true
|
|
6083
|
+
}).extend({ updatedAt: z.string() });
|
|
6084
|
+
function mcpBrief(brief, updatedAt) {
|
|
6085
|
+
return {
|
|
6086
|
+
summary: brief.summary,
|
|
6087
|
+
intent: brief.intent,
|
|
6088
|
+
outcome: brief.outcome,
|
|
6089
|
+
decisions: brief.decisions,
|
|
6090
|
+
openDecisions: brief.openDecisions,
|
|
6091
|
+
facts: brief.facts,
|
|
6092
|
+
links: brief.links,
|
|
6093
|
+
updatedAt
|
|
6094
|
+
};
|
|
6095
|
+
}
|
|
6096
|
+
const RECENT_EVENT_LIMIT = 100;
|
|
6097
|
+
/**
|
|
6098
|
+
* Register `getConversation`. It returns metadata, the Brief, and the last
|
|
6099
|
+
* Reply, not the whole transcript.
|
|
6100
|
+
*/
|
|
6101
|
+
function registerGetConversationTool(server, { user }) {
|
|
6102
|
+
server.registerTool("getConversation", {
|
|
6103
|
+
title: "Get conversation",
|
|
6104
|
+
description: "Get one conversation: its metadata, its Brief (a summary of intent, outcome, and state), and the last assistant reply. Use listConversationMessages for the full transcript.",
|
|
6105
|
+
inputSchema: { conversationId: mcpConversationIdSchema },
|
|
6106
|
+
outputSchema: {
|
|
6107
|
+
conversation: mcpConversationSchema,
|
|
6108
|
+
brief: mcpBriefSchema.optional(),
|
|
6109
|
+
lastReply: mcpMessageSchema.optional()
|
|
6110
|
+
},
|
|
6111
|
+
annotations: {
|
|
6112
|
+
destructiveHint: false,
|
|
6113
|
+
idempotentHint: true,
|
|
6114
|
+
openWorldHint: false,
|
|
6115
|
+
readOnlyHint: true
|
|
6116
|
+
}
|
|
6117
|
+
}, async ({ conversationId }) => {
|
|
6118
|
+
const detail = await isConversationParticipant(user, conversationId) ? await readConversationDetail(conversationId, {
|
|
6119
|
+
limit: RECENT_EVENT_LIMIT,
|
|
6120
|
+
viewer: user
|
|
6121
|
+
}) : void 0;
|
|
6122
|
+
if (!detail) return mcpToolError("Conversation not found.");
|
|
6123
|
+
const lastReply = mcpMessages(detail.events).filter((message) => message.role === "assistant").at(-1);
|
|
6124
|
+
return mcpToolResult({
|
|
6125
|
+
conversation: mcpConversation(detail),
|
|
6126
|
+
...detail.brief ? { brief: mcpBrief(detail.brief.content, detail.brief.updatedAt) } : void 0,
|
|
6127
|
+
...lastReply ? { lastReply } : void 0
|
|
6128
|
+
});
|
|
6129
|
+
});
|
|
6130
|
+
}
|
|
6131
|
+
// src/api/mcp/review-item.ts
|
|
6132
|
+
/**
|
|
6133
|
+
* Own the MCP view of one Review item, shared by the review item tools.
|
|
6134
|
+
*
|
|
6135
|
+
* MCP Review items stay read-only. An answer or dismissal sends a message to
|
|
6136
|
+
* the linked Conversations, which can start a Turn.
|
|
6137
|
+
*/
|
|
6138
|
+
/** MCP view of one Review item. Answers happen in Slack or the dashboard. */
|
|
6139
|
+
const mcpReviewItemSchema = z.object({
|
|
6140
|
+
id: z.string(),
|
|
6141
|
+
kind: z.enum(["notice", "action"]),
|
|
6142
|
+
status: z.enum([
|
|
6143
|
+
"open",
|
|
6144
|
+
"done",
|
|
6145
|
+
"dismissed"
|
|
6146
|
+
]),
|
|
6147
|
+
title: z.string(),
|
|
6148
|
+
body: z.string(),
|
|
6149
|
+
question: z.string().optional(),
|
|
6150
|
+
assignees: z.array(z.string()).describe("Names or emails. Empty when anyone can act."),
|
|
6151
|
+
canAct: z.boolean().describe("True when you can answer or dismiss it."),
|
|
6152
|
+
conversationIds: z.array(z.string()),
|
|
6153
|
+
subject: z.object({
|
|
6154
|
+
id: z.string(),
|
|
6155
|
+
title: z.string().optional(),
|
|
6156
|
+
url: z.string().optional()
|
|
6157
|
+
}).strict().optional(),
|
|
6158
|
+
createdAt: z.string(),
|
|
6159
|
+
updatedAt: z.string(),
|
|
6160
|
+
url: z.string().optional().describe("Dashboard link to answer it.")
|
|
6161
|
+
}).strict();
|
|
6162
|
+
/** Map a Review item to its MCP view for one user. */
|
|
6163
|
+
function mcpReviewItem(item, userId) {
|
|
6164
|
+
const url = getDashboardReviewItemLink(item.id);
|
|
6165
|
+
return {
|
|
6166
|
+
id: item.id,
|
|
6167
|
+
kind: item.kind,
|
|
6168
|
+
status: item.status,
|
|
6169
|
+
title: item.title,
|
|
6170
|
+
body: item.body,
|
|
6171
|
+
...item.request ? { question: item.request.question } : void 0,
|
|
6172
|
+
assignees: item.assignedToEveryone ? [] : item.assignees.map((person) => person.displayName ?? person.email),
|
|
6173
|
+
canAct: canActOnReviewItem(item, userId),
|
|
6174
|
+
conversationIds: item.conversationIds,
|
|
6175
|
+
...item.subject ? { subject: {
|
|
6176
|
+
id: item.subject.id,
|
|
6177
|
+
...item.subject.title ? { title: item.subject.title } : void 0,
|
|
6178
|
+
...item.subject.url ? { url: item.subject.url } : void 0
|
|
6179
|
+
} } : void 0,
|
|
6180
|
+
createdAt: item.createdAt.toISOString(),
|
|
6181
|
+
updatedAt: item.updatedAt.toISOString(),
|
|
6182
|
+
...url ? { url } : void 0
|
|
6183
|
+
};
|
|
6184
|
+
}
|
|
6185
|
+
// src/api/mcp/tools/get-review-item.ts
|
|
6186
|
+
/** Register `getReviewItem`, which reads one Review item in any status. */
|
|
6187
|
+
function registerGetReviewItemTool(server, { user }) {
|
|
6188
|
+
server.registerTool("getReviewItem", {
|
|
6189
|
+
title: "Get review item",
|
|
6190
|
+
description: "Get one review item, open or closed.",
|
|
6191
|
+
inputSchema: { id: z.string().min(1).describe("Review item id from searchReviewItems.") },
|
|
6192
|
+
outputSchema: { item: mcpReviewItemSchema },
|
|
6193
|
+
annotations: {
|
|
6194
|
+
destructiveHint: false,
|
|
6195
|
+
idempotentHint: true,
|
|
6196
|
+
openWorldHint: false,
|
|
6197
|
+
readOnlyHint: true
|
|
6198
|
+
}
|
|
6199
|
+
}, async ({ id }) => {
|
|
6200
|
+
try {
|
|
6201
|
+
return mcpToolResult({ item: mcpReviewItem(await readReviewItemForUser(user, id), user.id) });
|
|
6202
|
+
} catch (error) {
|
|
6203
|
+
if (error instanceof ReviewItemActionError) return mcpToolError(error.message);
|
|
6204
|
+
throw error;
|
|
6205
|
+
}
|
|
6206
|
+
});
|
|
6207
|
+
}
|
|
6208
|
+
// src/api/mcp/tools/search-automations.ts
|
|
6209
|
+
/** Register `searchAutomations`, which searches the Automations the user can see. */
|
|
6210
|
+
function registerSearchAutomationsTool(server, { user }) {
|
|
6211
|
+
const listQuery = automationListQuerySchema.shape;
|
|
6212
|
+
server.registerTool("searchAutomations", {
|
|
6213
|
+
title: "Search automations",
|
|
6214
|
+
description: "Search the automations that you can see, newest first. An automation is a saved instruction that runs on a schedule or on events.",
|
|
6215
|
+
inputSchema: {
|
|
6216
|
+
q: listQuery.q.describe("Text to search for in titles and instructions."),
|
|
6217
|
+
scope: z.enum([
|
|
6218
|
+
"all",
|
|
6219
|
+
"mine",
|
|
6220
|
+
"attention"
|
|
6221
|
+
]).default("all").describe("`mine` lists the ones you created. `attention` lists the ones that are blocked, cannot run, or had a problem in their last run."),
|
|
6222
|
+
state: listQuery.state.describe("Filter by state."),
|
|
6223
|
+
page: listQuery.page.describe("Page number, from 1.")
|
|
6224
|
+
},
|
|
6225
|
+
outputSchema: {
|
|
6226
|
+
automations: z.array(mcpAutomationSchema),
|
|
6227
|
+
total: z.number().int().describe("Matches across every page.")
|
|
6228
|
+
},
|
|
6229
|
+
annotations: {
|
|
6230
|
+
destructiveHint: false,
|
|
6231
|
+
idempotentHint: true,
|
|
6232
|
+
openWorldHint: false,
|
|
6233
|
+
readOnlyHint: true
|
|
6234
|
+
}
|
|
6235
|
+
}, async ({ page, q, scope, state }) => {
|
|
6236
|
+
const list = await readViewerAutomations(user, automationListQuerySchema.parse({
|
|
6237
|
+
page,
|
|
6238
|
+
q,
|
|
6239
|
+
scope,
|
|
6240
|
+
state
|
|
6241
|
+
}));
|
|
6242
|
+
return mcpToolResult({
|
|
6243
|
+
automations: list.automations.map(mcpAutomation),
|
|
6244
|
+
total: list.total
|
|
6245
|
+
});
|
|
6246
|
+
});
|
|
6247
|
+
}
|
|
6248
|
+
// src/api/mcp/tools/list-conversation-messages.ts
|
|
6249
|
+
const EVENT_SCAN_LIMIT = 200;
|
|
6250
|
+
/** Register `listConversationMessages`, which pages a transcript backward. */
|
|
6251
|
+
function registerListConversationMessagesTool(server, { user }) {
|
|
6252
|
+
server.registerTool("listConversationMessages", {
|
|
6253
|
+
title: "List conversation messages",
|
|
6254
|
+
description: "List the user and assistant messages of one conversation, oldest first, ending with the newest. Tool calls are not included.",
|
|
6255
|
+
inputSchema: {
|
|
6256
|
+
conversationId: mcpConversationIdSchema,
|
|
6257
|
+
before: z.string().min(1).max(1e3).optional().describe("`nextCursor` from an earlier call. Lists the messages before it."),
|
|
6258
|
+
limit: z.number().int().min(1).max(50).default(10).describe("Maximum number of messages.")
|
|
6259
|
+
},
|
|
6260
|
+
outputSchema: {
|
|
6261
|
+
messages: z.array(mcpMessageSchema),
|
|
6262
|
+
nextCursor: z.string().optional().describe("Set when older messages can exist.")
|
|
6263
|
+
},
|
|
6264
|
+
annotations: {
|
|
6265
|
+
destructiveHint: false,
|
|
6266
|
+
idempotentHint: true,
|
|
6267
|
+
openWorldHint: false,
|
|
6268
|
+
readOnlyHint: true
|
|
6269
|
+
}
|
|
6270
|
+
}, async ({ before, conversationId, limit }) => {
|
|
6271
|
+
const record = await isConversationParticipant(user, conversationId) ? await readConversationRecordFromSql(conversationId) : void 0;
|
|
6272
|
+
if (!record) return mcpToolError("Conversation not found.");
|
|
6273
|
+
const beforeSeq = before ? decodeConversationCursor({
|
|
6274
|
+
conversationId,
|
|
6275
|
+
cursor: before
|
|
6276
|
+
})?.seq : void 0;
|
|
6277
|
+
if (before && beforeSeq === void 0) return mcpToolError("The cursor is not valid. Omit `before` to read the newest messages.");
|
|
6278
|
+
if (record.conversation.transcriptPurgedAtMs !== void 0) return mcpToolResult({ messages: [] });
|
|
6279
|
+
const page = await readConversationEventPage(getSqlExecutor(), {
|
|
6280
|
+
beforeSeq,
|
|
6281
|
+
canExposePayload: true,
|
|
6282
|
+
conversationId,
|
|
6283
|
+
limit: EVENT_SCAN_LIMIT
|
|
6284
|
+
});
|
|
6285
|
+
const scanned = mcpMessages(page.events);
|
|
6286
|
+
const messages = scanned.slice(-limit);
|
|
6287
|
+
const olderSeq = scanned.length > messages.length ? messages[0]?.seq : page.previousSeq;
|
|
6288
|
+
return mcpToolResult({
|
|
6289
|
+
messages,
|
|
6290
|
+
...olderSeq === void 0 ? void 0 : { nextCursor: encodeConversationCursor({
|
|
6291
|
+
conversationId,
|
|
6292
|
+
seq: olderSeq
|
|
6293
|
+
}) }
|
|
6294
|
+
});
|
|
6295
|
+
});
|
|
6296
|
+
}
|
|
6297
|
+
// src/api/mcp/tools/search-conversations.ts
|
|
6298
|
+
/** Register `searchConversations`, which searches the Conversations of the user. */
|
|
6299
|
+
function registerSearchConversationsTool(server, { user }) {
|
|
5830
6300
|
const feedQuery = conversationFeedQuerySchema.shape;
|
|
5831
|
-
server.registerTool("
|
|
5832
|
-
title: "
|
|
5833
|
-
description: "
|
|
6301
|
+
server.registerTool("searchConversations", {
|
|
6302
|
+
title: "Search conversations",
|
|
6303
|
+
description: "Search your conversations and return up to 50, newest activity first. Only conversations that you started or take part in match.",
|
|
5834
6304
|
inputSchema: {
|
|
5835
6305
|
q: feedQuery.q.describe("Text to search for in titles and Briefs."),
|
|
5836
|
-
status: feedQuery.status.describe("
|
|
6306
|
+
status: feedQuery.status.describe("Return conversations that you have not archived (default) or that you archived. A search ignores this filter.")
|
|
5837
6307
|
},
|
|
5838
6308
|
outputSchema: { conversations: z.array(mcpConversationSchema) },
|
|
5839
6309
|
annotations: {
|
|
5840
|
-
|
|
5841
|
-
|
|
6310
|
+
destructiveHint: false,
|
|
6311
|
+
idempotentHint: true,
|
|
6312
|
+
openWorldHint: false,
|
|
6313
|
+
readOnlyHint: true
|
|
5842
6314
|
}
|
|
5843
6315
|
}, async ({ q, status }) => {
|
|
5844
|
-
|
|
6316
|
+
return mcpToolResult({ conversations: (await readConversationFeed({
|
|
5845
6317
|
q,
|
|
5846
6318
|
status,
|
|
5847
6319
|
viewer: user
|
|
5848
|
-
})).conversations.map(mcpConversation) };
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
6320
|
+
})).conversations.map(mcpConversation) });
|
|
6321
|
+
});
|
|
6322
|
+
}
|
|
6323
|
+
// src/api/mcp/tools/search-review-items.ts
|
|
6324
|
+
/** Register `searchReviewItems`, which searches open Review items for the user. */
|
|
6325
|
+
function registerSearchReviewItemsTool(server, { user }) {
|
|
6326
|
+
server.registerTool("searchReviewItems", {
|
|
6327
|
+
title: "Search review items",
|
|
6328
|
+
description: "Search the open review items that you can see, newest change first. A review item is a question or notice that waits for a person.",
|
|
6329
|
+
inputSchema: {
|
|
6330
|
+
q: z.string().trim().min(1).max(200).optional().describe("Words to find in the title or body."),
|
|
6331
|
+
conversationId: z.string().min(1).optional().describe("Return only the items linked to this conversation."),
|
|
6332
|
+
limit: z.number().int().min(1).max(50).default(20).describe("Maximum number of items.")
|
|
6333
|
+
},
|
|
6334
|
+
outputSchema: {
|
|
6335
|
+
items: z.array(mcpReviewItemSchema),
|
|
6336
|
+
truncated: z.boolean().describe("True when more items match.")
|
|
6337
|
+
},
|
|
6338
|
+
annotations: {
|
|
6339
|
+
destructiveHint: false,
|
|
6340
|
+
idempotentHint: true,
|
|
6341
|
+
openWorldHint: false,
|
|
6342
|
+
readOnlyHint: true
|
|
6343
|
+
}
|
|
6344
|
+
}, async ({ conversationId, limit, q }) => {
|
|
6345
|
+
const items = await searchReviewItems(getDb(), reviewItemReaderForUser(user), {
|
|
6346
|
+
...conversationId ? { conversationId } : void 0,
|
|
6347
|
+
limit: limit + 1,
|
|
6348
|
+
...q ? { query: q } : void 0,
|
|
6349
|
+
statuses: ["open"]
|
|
6350
|
+
});
|
|
6351
|
+
return mcpToolResult({
|
|
6352
|
+
items: items.slice(0, limit).map((item) => mcpReviewItem(item, user.id)),
|
|
6353
|
+
truncated: items.length > limit
|
|
6354
|
+
});
|
|
6355
|
+
});
|
|
6356
|
+
}
|
|
6357
|
+
// src/api/mcp/tools/stop-conversation.ts
|
|
6358
|
+
/** Register `stopConversation`, which stops the active Turn. */
|
|
6359
|
+
function registerStopConversationTool(server, { queue, user }) {
|
|
6360
|
+
server.registerTool("stopConversation", {
|
|
6361
|
+
title: "Stop conversation",
|
|
6362
|
+
description: "Stop the running turn of one of your conversations. Messages that wait for the turn are dropped. It starts no new work.",
|
|
6363
|
+
inputSchema: { conversationId: mcpConversationIdSchema },
|
|
6364
|
+
outputSchema: {
|
|
6365
|
+
status: z.enum(["no_work", "requested"]).describe("`no_work` when nothing was running."),
|
|
6366
|
+
droppedMessages: z.number().int()
|
|
6367
|
+
},
|
|
6368
|
+
annotations: {
|
|
6369
|
+
destructiveHint: true,
|
|
6370
|
+
idempotentHint: true,
|
|
6371
|
+
openWorldHint: false,
|
|
6372
|
+
readOnlyHint: false
|
|
6373
|
+
}
|
|
6374
|
+
}, async ({ conversationId }) => {
|
|
6375
|
+
if (!await isConversationParticipant(user, conversationId)) return mcpToolError("Conversation not found.");
|
|
6376
|
+
try {
|
|
6377
|
+
const result = await stopConversationTurnForViewer(user, conversationId, { queue });
|
|
6378
|
+
return mcpToolResult({
|
|
6379
|
+
status: result.status,
|
|
6380
|
+
droppedMessages: result.stoppedInboundMessageIds.length
|
|
6381
|
+
});
|
|
6382
|
+
} catch (error) {
|
|
6383
|
+
return apiToolError(error);
|
|
6384
|
+
}
|
|
5856
6385
|
});
|
|
5857
6386
|
}
|
|
5858
6387
|
// src/api/mcp/server.ts
|
|
@@ -5864,18 +6393,31 @@ function registerListConversationsTool(server, user) {
|
|
|
5864
6393
|
* JSON. No MCP session or SSE stream outlives the request, so the route works
|
|
5865
6394
|
* on any app instance and is not limited by the live-stream duration.
|
|
5866
6395
|
*/
|
|
5867
|
-
/**
|
|
5868
|
-
|
|
6396
|
+
/** The tools that each scope unlocks. A client sees only its granted tools. */
|
|
6397
|
+
const TOOLS_BY_SCOPE = {
|
|
6398
|
+
"conversations:read": [
|
|
6399
|
+
registerSearchConversationsTool,
|
|
6400
|
+
registerGetConversationTool,
|
|
6401
|
+
registerListConversationMessagesTool
|
|
6402
|
+
],
|
|
6403
|
+
"conversations:manage": [registerArchiveConversationTool, registerStopConversationTool],
|
|
6404
|
+
"review_items:read": [registerSearchReviewItemsTool, registerGetReviewItemTool],
|
|
6405
|
+
"automations:read": [registerSearchAutomationsTool, registerGetAutomationTool]
|
|
6406
|
+
};
|
|
6407
|
+
const INSTRUCTIONS = "Junior is a Slack agent. These tools read your Junior conversations, review items, and automations. They cannot send a message or start agent work. To ask Junior for work or to answer a review item, use the Slack or dashboard link in a result.";
|
|
6408
|
+
/** Build the MCP server for one authenticated user and granted scopes. */
|
|
6409
|
+
function createMcpServer(context, scopes) {
|
|
5869
6410
|
const server = new McpServer({
|
|
5870
6411
|
name: "junior",
|
|
5871
6412
|
version: JUNIOR_VERSION
|
|
5872
|
-
});
|
|
5873
|
-
|
|
6413
|
+
}, { instructions: INSTRUCTIONS });
|
|
6414
|
+
for (const scope of scopes) for (const register of TOOLS_BY_SCOPE[scope]) register(server, context);
|
|
5874
6415
|
return server;
|
|
5875
6416
|
}
|
|
5876
6417
|
/** Answer one Streamable HTTP POST for an authenticated user. */
|
|
5877
|
-
async function handleMcpServerRequest(request,
|
|
5878
|
-
const
|
|
6418
|
+
async function handleMcpServerRequest(request, args) {
|
|
6419
|
+
const { scopes, ...context } = args;
|
|
6420
|
+
const server = createMcpServer(context, scopes);
|
|
5879
6421
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
5880
6422
|
enableJsonResponse: true,
|
|
5881
6423
|
sessionIdGenerator: void 0
|
|
@@ -5972,7 +6514,7 @@ function unauthorized(urls, sentToken) {
|
|
|
5972
6514
|
status: 401,
|
|
5973
6515
|
headers: {
|
|
5974
6516
|
"cache-control": "no-store",
|
|
5975
|
-
"www-authenticate": `Bearer ${sentToken ? "error=\"invalid_token\", " : ""}resource_metadata="${urls.resourceMetadata}", scope="${
|
|
6517
|
+
"www-authenticate": `Bearer ${sentToken ? "error=\"invalid_token\", " : ""}resource_metadata="${urls.resourceMetadata}", scope="${MCP_ALL_SCOPES}"`
|
|
5976
6518
|
}
|
|
5977
6519
|
});
|
|
5978
6520
|
}
|
|
@@ -6001,18 +6543,22 @@ function createMcpHttpHandler(options) {
|
|
|
6001
6543
|
], MCP_PATH, async (c) => {
|
|
6002
6544
|
const requestUrls = urls(c.req.raw);
|
|
6003
6545
|
const token = bearerToken(c.req.raw);
|
|
6004
|
-
const
|
|
6546
|
+
const grant = token ? await authenticateMcpAccessToken({
|
|
6005
6547
|
resource: requestUrls.resource,
|
|
6006
6548
|
state: options.state,
|
|
6007
6549
|
token
|
|
6008
6550
|
}) : void 0;
|
|
6009
|
-
const user =
|
|
6010
|
-
if (!user) return unauthorized(requestUrls, token !== void 0);
|
|
6551
|
+
const user = grant && isAllowedSignInEmail(grant.email, options.allowedDomains) ? await resolveViewerUser(grant.email) : void 0;
|
|
6552
|
+
if (!grant || !user) return unauthorized(requestUrls, token !== void 0);
|
|
6011
6553
|
if (c.req.method !== "POST") return new Response(null, {
|
|
6012
6554
|
status: 405,
|
|
6013
6555
|
headers: { allow: "POST" }
|
|
6014
6556
|
});
|
|
6015
|
-
return await handleMcpServerRequest(c.req.raw,
|
|
6557
|
+
return await handleMcpServerRequest(c.req.raw, {
|
|
6558
|
+
queue: options.queue,
|
|
6559
|
+
scopes: grant.scopes,
|
|
6560
|
+
user
|
|
6561
|
+
});
|
|
6016
6562
|
});
|
|
6017
6563
|
return app.fetch;
|
|
6018
6564
|
}
|
|
@@ -6602,7 +7148,6 @@ async function createApp(options) {
|
|
|
6602
7148
|
setBotModelConfig(options ?? {});
|
|
6603
7149
|
setRuntimeLimits(options?.limits);
|
|
6604
7150
|
setSlackReactionConfig(options?.slack ?? {});
|
|
6605
|
-
if (options?.slack?.crossActorMidRunMode) setCrossActorMidRunMode(options.slack.crossActorMidRunMode);
|
|
6606
7151
|
assertNoLegacyAllowedDomains(Object.keys(dashboard ?? {}));
|
|
6607
7152
|
allowedDomains = resolveAllowedDomains(options?.allowedDomains);
|
|
6608
7153
|
if (allowedDomains.length === 0) logWarn("app.allowed_domains.unset", { "app.reason": "allowedDomains is empty, so any Slack workspace member can use Junior" });
|
|
@@ -6750,6 +7295,7 @@ async function createApp(options) {
|
|
|
6750
7295
|
handleMcpRequest ??= createMcpHttpHandler({
|
|
6751
7296
|
allowedDomains,
|
|
6752
7297
|
baseURL: resolvePublicBaseURL(dashboard),
|
|
7298
|
+
queue: conversationWorkQueue,
|
|
6753
7299
|
state: getMcpState()
|
|
6754
7300
|
});
|
|
6755
7301
|
return handleMcpRequest(c.req.raw);
|