@sentry/junior 0.271.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-Bqbtr1RV.js → adapter-umap1cth.js} +1 -1
- package/dist/{admin-CWBjzyN0.js → admin-0o6E_3lo.js} +1 -1
- package/dist/{agent-runner-BiHsH8O_.js → agent-runner-D7Qz-lGO.js} +899 -631
- package/dist/api/schema.d.ts +2 -2
- package/dist/api/schema.js +5 -5
- package/dist/api.d.ts +1 -1
- package/dist/api.js +62 -537
- package/dist/{app-C81xfpNa.d.ts → app-BcyhJq8x.d.ts} +4 -9
- package/dist/app.d.ts +1 -1
- package/dist/app.js +673 -134
- package/dist/{assistant-reply-DgtX9DaJ.js → assistant-reply-wZ7i4OFa.js} +1 -1
- package/dist/{automation-Dwh_xcqM.js → automation-yffXcZt9.js} +1 -1
- package/dist/{briefs-0FeSJHQ3.js → briefs-CgMnlpIp.js} +4 -4
- package/dist/{catalog-runtime-Bw9kjGVu.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-B0jlQSgO.js → client-DhqbV0Fe.js} +63 -5
- package/dist/{reporting-schema-CZREhYc-.js → common-Eu6sx09c.js} +4 -1
- package/dist/{config-DrAeD8sr.js → config-C6FTd7PX.js} +7 -15
- package/dist/{connections-BzN2K0G9.js → connections-Dq72KajR.js} +11 -11
- package/dist/{conversation-AhOVzN_k.js → conversation-T4oabbBi.js} +22 -1
- package/dist/{conversation-privacy-D1NhUxyo.js → conversation-privacy-BeRgrsph.js} +1 -1
- package/dist/{credential-sync-DpNWl4yy.js → credential-sync-BmCbHAp4.js} +15 -16
- package/dist/{db-DSabI7jJ.d.ts → db-DOzBGLn5.d.ts} +11 -1
- package/dist/{executor-C2cRw0hZ.js → executor-BafgvQcc.js} +25 -7
- package/dist/instrumentation.js +114 -3
- package/dist/{job-queue-CEeLYkSj.js → job-queue-C4lphW9Y.js} +3 -3
- package/dist/{logging-DuwXUuIY.js → logging-CNs1e04n.js} +6 -1
- package/dist/message-CETC5lsl.js +186 -0
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-callback-server-oM5TJt34.js → oauth-callback-server-DcQZsKMm.js} +2 -2
- package/dist/{credentialed-mh7HBqey.js → projection-BOpi891B.js} +4576 -49
- package/dist/{prompt-BPSdXYXV.js → prompt-B1ySjUyf.js} +1 -1
- package/dist/{registry-BPtbAVFo.js → registry-B-yUrv3h.js} +2 -2
- package/dist/{resolve-D44G1jJm.js → resolve-DqVtd4Q9.js} +3 -3
- package/dist/{result-CvP8c5bb.js → result-DQwTiLTw.js} +16 -12
- package/dist/{runner-FIGIKvk2.js → runner-Dco5qMq6.js} +11 -11
- package/dist/{sandbox-egress-signals-C_JzucwP.js → sandbox-egress-signals-DFc_9Mcv.js} +1 -1
- package/dist/{schema-DKZgO-xf.js → schema-5xHDGlBW.js} +10 -9
- package/dist/{schema-DPCvePop.d.ts → schema-9OGdsaxF.d.ts} +28 -3
- package/dist/{service-account-1mJGRAvx.js → service-account-CWfk9Cs-.js} +3 -3
- package/dist/{service-account-CHO3YXpt.js → service-account-smdPFWNy.js} +3 -3
- package/dist/{session-stop-DuUB1rwQ.js → session-stop-DaT_7tYa.js} +50 -24
- package/dist/{skills-DHK7L9Eo.js → skills-BbtJSF2-.js} +2 -2
- package/dist/{spawn-CKl_ySIq.js → spawn-Ceikdjkp.js} +2 -2
- package/dist/{list-DjJJ5K0x.js → stop-dKUc-x2K.js} +1017 -469
- package/dist/{store-Di0E4EvU.js → store-C1xuZcLd.js} +48 -31
- package/dist/{store-BF2XEeQz.js → store-DaxUuOmD.js} +3 -3
- package/dist/{task-queue-CC7IFtbT.js → task-queue-ymAh_-P1.js} +4 -4
- package/dist/{task-runner-DKZtMFwa.js → task-runner-vlH6pEm3.js} +6 -6
- package/dist/{thread-state-BQ1T3eeF.js → thread-state-BkUpE9Ae.js} +4 -5
- package/dist/{turn-failure-response-B5eSFoHn.js → turn-failure-response-DIsD7ip_.js} +7 -8
- package/dist/{turn-lifecycle-CTLEIEOn.js → turn-lifecycle-BdPKdPS8.js} +1 -1
- package/dist/turn-session-routing-BdCTDq9m.js +1625 -0
- package/dist/{validation-Cjo-3yt3.js → validation-BQjCfh84.js} +1 -1
- package/dist/{version-D30MBLLw.js → version-BpiFGZZW.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-D3jhuP66.js → viewer-C6Y1aXjZ.js} +49 -21
- package/dist/{work-CL9QwQcl.js → work-ChPh_YEh.js} +27 -39
- package/dist/{worker-S2P7IJDI.js → worker-Be_sK_ou.js} +6 -6
- package/migrations/0072_conversation_read_positions.sql +10 -0
- package/migrations/meta/0072_snapshot.json +4064 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +4 -4
- package/dist/message-CRe3WBKc.js +0 -59
- package/dist/turn-session-routing-BFVp8TbU.js +0 -6072
- package/migrations/AGENTS.md +0 -8
- 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 {
|
|
10
|
-
import { n as pluginCatalogRuntime } from "./catalog-runtime-
|
|
11
|
-
import {
|
|
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-mh7HBqey.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,
|
|
17
|
-
import { b as listPendingAgentInvocationMailboxAppends, x as listPendingAgentInvocationResults } from "./store-
|
|
18
|
-
import {
|
|
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";
|
|
@@ -1924,13 +1925,6 @@ async function handleSlackReviewItemClick(args) {
|
|
|
1924
1925
|
services,
|
|
1925
1926
|
target
|
|
1926
1927
|
};
|
|
1927
|
-
if (click.kind === "done") {
|
|
1928
|
-
await act({
|
|
1929
|
-
...common,
|
|
1930
|
-
action: "complete"
|
|
1931
|
-
});
|
|
1932
|
-
return;
|
|
1933
|
-
}
|
|
1934
1928
|
if (click.kind === "dismiss") {
|
|
1935
1929
|
await act({
|
|
1936
1930
|
...common,
|
|
@@ -5294,59 +5288,6 @@ function redirectUriMatches(registered, requested) {
|
|
|
5294
5288
|
return candidate !== null && isLoopbackHttp(candidate) && candidate.hostname === url.hostname && candidate.pathname === url.pathname && candidate.search === url.search;
|
|
5295
5289
|
});
|
|
5296
5290
|
}
|
|
5297
|
-
// src/api/mcp/oauth/metadata.ts
|
|
5298
|
-
/**
|
|
5299
|
-
* Own the MCP OAuth paths, scope, and discovery documents.
|
|
5300
|
-
*
|
|
5301
|
-
* Every URL comes from the trusted public origin of the app. The documents
|
|
5302
|
-
* advertise Client ID Metadata Documents and no client registration endpoint.
|
|
5303
|
-
*/
|
|
5304
|
-
const MCP_PATH = "/api/mcp";
|
|
5305
|
-
const MCP_SCOPE = "conversations:read";
|
|
5306
|
-
const MCP_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource/api/mcp";
|
|
5307
|
-
const MCP_AUTHORIZATION_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
5308
|
-
const MCP_AUTHORIZE_PATH = "/api/mcp/oauth/authorize";
|
|
5309
|
-
const MCP_TOKEN_PATH = "/api/mcp/oauth/token";
|
|
5310
|
-
/** Dashboard-authenticated consent page for one pending authorization. */
|
|
5311
|
-
const MCP_CONSENT_PATH = "/_junior/mcp/authorize/:authorizationId";
|
|
5312
|
-
/** Resolve the MCP URLs for this request from the trusted public origin. */
|
|
5313
|
-
function resolveMcpUrls(request, baseURL) {
|
|
5314
|
-
const origin = resolvePublicOrigin(request, baseURL);
|
|
5315
|
-
if (!origin) throw new Error("Junior MCP requires a configured public base URL");
|
|
5316
|
-
const url = (path) => new URL(path, origin).toString();
|
|
5317
|
-
return {
|
|
5318
|
-
authorize: url(MCP_AUTHORIZE_PATH),
|
|
5319
|
-
issuer: origin.origin,
|
|
5320
|
-
resource: url(MCP_PATH),
|
|
5321
|
-
resourceMetadata: url(MCP_RESOURCE_METADATA_PATH),
|
|
5322
|
-
token: url(MCP_TOKEN_PATH),
|
|
5323
|
-
consent: (authorizationId) => url(MCP_CONSENT_PATH.replace(":authorizationId", encodeURIComponent(authorizationId)))
|
|
5324
|
-
};
|
|
5325
|
-
}
|
|
5326
|
-
/** RFC 9728 protected resource metadata for `/api/mcp`. */
|
|
5327
|
-
function protectedResourceMetadata(urls) {
|
|
5328
|
-
return {
|
|
5329
|
-
resource: urls.resource,
|
|
5330
|
-
authorization_servers: [urls.issuer],
|
|
5331
|
-
bearer_methods_supported: ["header"],
|
|
5332
|
-
scopes_supported: [MCP_SCOPE]
|
|
5333
|
-
};
|
|
5334
|
-
}
|
|
5335
|
-
/** RFC 8414 authorization server metadata. CIMD is the only client model. */
|
|
5336
|
-
function authorizationServerMetadata(urls) {
|
|
5337
|
-
return {
|
|
5338
|
-
issuer: urls.issuer,
|
|
5339
|
-
authorization_endpoint: urls.authorize,
|
|
5340
|
-
token_endpoint: urls.token,
|
|
5341
|
-
scopes_supported: [MCP_SCOPE],
|
|
5342
|
-
response_types_supported: ["code"],
|
|
5343
|
-
grant_types_supported: ["authorization_code", "refresh_token"],
|
|
5344
|
-
code_challenge_methods_supported: ["S256"],
|
|
5345
|
-
token_endpoint_auth_methods_supported: ["none"],
|
|
5346
|
-
authorization_response_iss_parameter_supported: true,
|
|
5347
|
-
client_id_metadata_document_supported: true
|
|
5348
|
-
};
|
|
5349
|
-
}
|
|
5350
5291
|
// src/api/mcp/oauth/page.ts
|
|
5351
5292
|
/** Render one MCP sign-in page. It sends no referrer and loads no resources. */
|
|
5352
5293
|
function renderMcpPage(page) {
|
|
@@ -5362,6 +5303,42 @@ function renderMcpPage(page) {
|
|
|
5362
5303
|
title: page.title
|
|
5363
5304
|
});
|
|
5364
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);
|
|
5365
5342
|
// src/api/mcp/oauth/authorization.ts
|
|
5366
5343
|
/**
|
|
5367
5344
|
* Own the OAuth authorization endpoint and its pending requests.
|
|
@@ -5383,7 +5360,7 @@ const mcpAuthorizationSchema = z.object({
|
|
|
5383
5360
|
expiresAtMs: z.number().int().positive(),
|
|
5384
5361
|
redirectUri: z.string().min(1),
|
|
5385
5362
|
resource: z.string().min(1),
|
|
5386
|
-
scope:
|
|
5363
|
+
scope: mcpScopeValueSchema,
|
|
5387
5364
|
state: z.string().max(STATE_MAX_LENGTH).optional()
|
|
5388
5365
|
}).strict();
|
|
5389
5366
|
/** The user who owns one consent page, and the hash of its form token. */
|
|
@@ -5490,8 +5467,8 @@ async function handleMcpAuthorize(args) {
|
|
|
5490
5467
|
const codeChallenge = params.get("code_challenge") ?? "";
|
|
5491
5468
|
if (params.get("code_challenge_method") !== "S256" || !CODE_CHALLENGE_PATTERN.test(codeChallenge)) return fail("invalid_request", "PKCE with S256 is required.");
|
|
5492
5469
|
if (oauthState !== void 0 && oauthState.length > STATE_MAX_LENGTH) return fail("invalid_request", "The state parameter is too long.");
|
|
5493
|
-
const
|
|
5494
|
-
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}.`);
|
|
5495
5472
|
if (params.get("resource") !== args.urls.resource) return fail("invalid_target", `The resource must be ${args.urls.resource}.`);
|
|
5496
5473
|
const authorizationId = randomBytes(32).toString("base64url");
|
|
5497
5474
|
const authorization = {
|
|
@@ -5501,7 +5478,7 @@ async function handleMcpAuthorize(args) {
|
|
|
5501
5478
|
expiresAtMs: Date.now() + AUTHORIZATION_TTL_MS,
|
|
5502
5479
|
redirectUri,
|
|
5503
5480
|
resource: args.urls.resource,
|
|
5504
|
-
scope:
|
|
5481
|
+
scope: scopes.join(" "),
|
|
5505
5482
|
state: oauthState
|
|
5506
5483
|
};
|
|
5507
5484
|
await args.state.set(authorizationKey(authorizationId), authorization, AUTHORIZATION_TTL_MS);
|
|
@@ -5536,7 +5513,7 @@ const authorizationCodeSchema = z.object({
|
|
|
5536
5513
|
grantId: z.string().uuid(),
|
|
5537
5514
|
redirectUri: z.string().min(1),
|
|
5538
5515
|
resource: z.string().min(1),
|
|
5539
|
-
scope:
|
|
5516
|
+
scope: mcpScopeValueSchema
|
|
5540
5517
|
}).strict();
|
|
5541
5518
|
const grantSchema = z.object({
|
|
5542
5519
|
clientId: z.string().min(1),
|
|
@@ -5544,7 +5521,7 @@ const grantSchema = z.object({
|
|
|
5544
5521
|
/** Absolute end of the grant. Refresh never moves it. */
|
|
5545
5522
|
expiresAtMs: z.number().int().positive(),
|
|
5546
5523
|
resource: z.string().min(1),
|
|
5547
|
-
scope:
|
|
5524
|
+
scope: mcpScopeValueSchema
|
|
5548
5525
|
}).strict();
|
|
5549
5526
|
const tokenSchema = z.object({ grantId: z.string().uuid() }).strict();
|
|
5550
5527
|
function codeKey(code) {
|
|
@@ -5579,7 +5556,7 @@ async function issueAuthorizationCode(args) {
|
|
|
5579
5556
|
grantId: randomUUID(),
|
|
5580
5557
|
redirectUri: args.redirectUri,
|
|
5581
5558
|
resource: args.resource,
|
|
5582
|
-
scope:
|
|
5559
|
+
scope: args.scope
|
|
5583
5560
|
};
|
|
5584
5561
|
await args.state.set(codeKey(code), record, CODE_TTL_MS);
|
|
5585
5562
|
return code;
|
|
@@ -5669,8 +5646,8 @@ async function refreshGrant(args) {
|
|
|
5669
5646
|
const token = await readRecord(args.state, key, tokenSchema);
|
|
5670
5647
|
const grant = token ? await readGrant(args.state, token.grantId) : void 0;
|
|
5671
5648
|
if (!token || !grant || grant.clientId !== args.clientId || args.resource !== void 0 && args.resource !== grant.resource) return invalidGrant("The refresh token is not valid.");
|
|
5672
|
-
if (args.scope !== void 0 && args.scope !== grant.scope) return {
|
|
5673
|
-
description: `
|
|
5649
|
+
if (args.scope !== void 0 && parseMcpScopes(args.scope)?.join(" ") !== grant.scope) return {
|
|
5650
|
+
description: `Refresh keeps the granted scope: ${grant.scope}.`,
|
|
5674
5651
|
error: "invalid_scope",
|
|
5675
5652
|
ok: false
|
|
5676
5653
|
};
|
|
@@ -5689,9 +5666,9 @@ async function refreshGrant(args) {
|
|
|
5689
5666
|
return await issueTokens(args.state, token.grantId, grant);
|
|
5690
5667
|
}
|
|
5691
5668
|
/**
|
|
5692
|
-
* Return the user email of one MCP access token for one
|
|
5693
|
-
* tokens and other bearer values never match, because
|
|
5694
|
-
* 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.
|
|
5695
5672
|
*/
|
|
5696
5673
|
async function authenticateMcpAccessToken(args) {
|
|
5697
5674
|
if (!args.token.startsWith(ACCESS_TOKEN_PREFIX)) return void 0;
|
|
@@ -5699,7 +5676,62 @@ async function authenticateMcpAccessToken(args) {
|
|
|
5699
5676
|
if (!token) return void 0;
|
|
5700
5677
|
const grant = await readGrant(args.state, token.grantId);
|
|
5701
5678
|
if (!grant || grant.resource !== args.resource) return void 0;
|
|
5702
|
-
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
|
+
};
|
|
5703
5735
|
}
|
|
5704
5736
|
// src/api/mcp/oauth/consent-page.ts
|
|
5705
5737
|
/**
|
|
@@ -5727,8 +5759,10 @@ function consentPage(args) {
|
|
|
5727
5759
|
const clientHost = new URL(args.authorization.clientId).host;
|
|
5728
5760
|
const clientName = args.authorization.clientName ?? clientHost;
|
|
5729
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("");
|
|
5730
5763
|
return renderMcpPage({
|
|
5731
|
-
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>
|
|
5732
5766
|
<form method="post">
|
|
5733
5767
|
<input type="hidden" name="csrf" value="${escapeHtml(args.csrfToken)}">
|
|
5734
5768
|
${button("approve", "Allow", true)}${button("deny", "Deny", false)}
|
|
@@ -5798,13 +5832,15 @@ async function handleMcpConsentPage(args) {
|
|
|
5798
5832
|
email: args.user.email,
|
|
5799
5833
|
redirectUri: authorization.redirectUri,
|
|
5800
5834
|
resource: authorization.resource,
|
|
5835
|
+
scope: authorization.scope,
|
|
5801
5836
|
state: args.state
|
|
5802
5837
|
}) },
|
|
5803
5838
|
redirectUri: authorization.redirectUri,
|
|
5804
5839
|
state: authorization.state
|
|
5805
5840
|
});
|
|
5806
5841
|
}
|
|
5807
|
-
// src/api/mcp/
|
|
5842
|
+
// src/api/mcp/conversation.ts
|
|
5843
|
+
/** Own the MCP view of one Conversation, shared by the conversation tools. */
|
|
5808
5844
|
/** MCP view of one Conversation. It omits dashboard-only fields. */
|
|
5809
5845
|
const mcpConversationSchema = z.object({
|
|
5810
5846
|
conversationId: z.string(),
|
|
@@ -5818,6 +5854,7 @@ const mcpConversationSchema = z.object({
|
|
|
5818
5854
|
archivedAt: z.string().optional(),
|
|
5819
5855
|
url: z.string().optional()
|
|
5820
5856
|
}).strict();
|
|
5857
|
+
/** Map a Conversation summary to its MCP view. */
|
|
5821
5858
|
function mcpConversation(summary) {
|
|
5822
5859
|
return {
|
|
5823
5860
|
conversationId: summary.conversationId,
|
|
@@ -5832,34 +5869,519 @@ function mcpConversation(summary) {
|
|
|
5832
5869
|
url: getDashboardConversationLink(summary.conversationId)
|
|
5833
5870
|
};
|
|
5834
5871
|
}
|
|
5835
|
-
/**
|
|
5836
|
-
|
|
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 }) {
|
|
5837
6300
|
const feedQuery = conversationFeedQuerySchema.shape;
|
|
5838
|
-
server.registerTool("
|
|
5839
|
-
title: "
|
|
5840
|
-
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.",
|
|
5841
6304
|
inputSchema: {
|
|
5842
6305
|
q: feedQuery.q.describe("Text to search for in titles and Briefs."),
|
|
5843
|
-
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.")
|
|
5844
6307
|
},
|
|
5845
6308
|
outputSchema: { conversations: z.array(mcpConversationSchema) },
|
|
5846
6309
|
annotations: {
|
|
5847
|
-
|
|
5848
|
-
|
|
6310
|
+
destructiveHint: false,
|
|
6311
|
+
idempotentHint: true,
|
|
6312
|
+
openWorldHint: false,
|
|
6313
|
+
readOnlyHint: true
|
|
5849
6314
|
}
|
|
5850
6315
|
}, async ({ q, status }) => {
|
|
5851
|
-
|
|
6316
|
+
return mcpToolResult({ conversations: (await readConversationFeed({
|
|
5852
6317
|
q,
|
|
5853
6318
|
status,
|
|
5854
6319
|
viewer: user
|
|
5855
|
-
})).conversations.map(mcpConversation) };
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
|
|
5860
|
-
|
|
5861
|
-
|
|
5862
|
-
|
|
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
|
+
}
|
|
5863
6385
|
});
|
|
5864
6386
|
}
|
|
5865
6387
|
// src/api/mcp/server.ts
|
|
@@ -5871,18 +6393,31 @@ function registerListConversationsTool(server, user) {
|
|
|
5871
6393
|
* JSON. No MCP session or SSE stream outlives the request, so the route works
|
|
5872
6394
|
* on any app instance and is not limited by the live-stream duration.
|
|
5873
6395
|
*/
|
|
5874
|
-
/**
|
|
5875
|
-
|
|
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) {
|
|
5876
6410
|
const server = new McpServer({
|
|
5877
6411
|
name: "junior",
|
|
5878
6412
|
version: JUNIOR_VERSION
|
|
5879
|
-
});
|
|
5880
|
-
|
|
6413
|
+
}, { instructions: INSTRUCTIONS });
|
|
6414
|
+
for (const scope of scopes) for (const register of TOOLS_BY_SCOPE[scope]) register(server, context);
|
|
5881
6415
|
return server;
|
|
5882
6416
|
}
|
|
5883
6417
|
/** Answer one Streamable HTTP POST for an authenticated user. */
|
|
5884
|
-
async function handleMcpServerRequest(request,
|
|
5885
|
-
const
|
|
6418
|
+
async function handleMcpServerRequest(request, args) {
|
|
6419
|
+
const { scopes, ...context } = args;
|
|
6420
|
+
const server = createMcpServer(context, scopes);
|
|
5886
6421
|
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
5887
6422
|
enableJsonResponse: true,
|
|
5888
6423
|
sessionIdGenerator: void 0
|
|
@@ -5979,7 +6514,7 @@ function unauthorized(urls, sentToken) {
|
|
|
5979
6514
|
status: 401,
|
|
5980
6515
|
headers: {
|
|
5981
6516
|
"cache-control": "no-store",
|
|
5982
|
-
"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}"`
|
|
5983
6518
|
}
|
|
5984
6519
|
});
|
|
5985
6520
|
}
|
|
@@ -6008,18 +6543,22 @@ function createMcpHttpHandler(options) {
|
|
|
6008
6543
|
], MCP_PATH, async (c) => {
|
|
6009
6544
|
const requestUrls = urls(c.req.raw);
|
|
6010
6545
|
const token = bearerToken(c.req.raw);
|
|
6011
|
-
const
|
|
6546
|
+
const grant = token ? await authenticateMcpAccessToken({
|
|
6012
6547
|
resource: requestUrls.resource,
|
|
6013
6548
|
state: options.state,
|
|
6014
6549
|
token
|
|
6015
6550
|
}) : void 0;
|
|
6016
|
-
const user =
|
|
6017
|
-
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);
|
|
6018
6553
|
if (c.req.method !== "POST") return new Response(null, {
|
|
6019
6554
|
status: 405,
|
|
6020
6555
|
headers: { allow: "POST" }
|
|
6021
6556
|
});
|
|
6022
|
-
return await handleMcpServerRequest(c.req.raw,
|
|
6557
|
+
return await handleMcpServerRequest(c.req.raw, {
|
|
6558
|
+
queue: options.queue,
|
|
6559
|
+
scopes: grant.scopes,
|
|
6560
|
+
user
|
|
6561
|
+
});
|
|
6023
6562
|
});
|
|
6024
6563
|
return app.fetch;
|
|
6025
6564
|
}
|
|
@@ -6609,7 +7148,6 @@ async function createApp(options) {
|
|
|
6609
7148
|
setBotModelConfig(options ?? {});
|
|
6610
7149
|
setRuntimeLimits(options?.limits);
|
|
6611
7150
|
setSlackReactionConfig(options?.slack ?? {});
|
|
6612
|
-
if (options?.slack?.crossActorMidRunMode) setCrossActorMidRunMode(options.slack.crossActorMidRunMode);
|
|
6613
7151
|
assertNoLegacyAllowedDomains(Object.keys(dashboard ?? {}));
|
|
6614
7152
|
allowedDomains = resolveAllowedDomains(options?.allowedDomains);
|
|
6615
7153
|
if (allowedDomains.length === 0) logWarn("app.allowed_domains.unset", { "app.reason": "allowedDomains is empty, so any Slack workspace member can use Junior" });
|
|
@@ -6757,6 +7295,7 @@ async function createApp(options) {
|
|
|
6757
7295
|
handleMcpRequest ??= createMcpHttpHandler({
|
|
6758
7296
|
allowedDomains,
|
|
6759
7297
|
baseURL: resolvePublicBaseURL(dashboard),
|
|
7298
|
+
queue: conversationWorkQueue,
|
|
6760
7299
|
state: getMcpState()
|
|
6761
7300
|
});
|
|
6762
7301
|
return handleMcpRequest(c.req.raw);
|