@mastra/factory 0.24.0-alpha.1 → 0.24.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -4
- package/dist/auth.d.ts +14 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +45 -9
- package/dist/auth.js.map +1 -1
- package/dist/boards/work-transition-policy.d.ts.map +1 -1
- package/dist/boards/work-transition-policy.js +10 -0
- package/dist/boards/work-transition-policy.js.map +1 -1
- package/dist/boards/work.d.ts +14 -0
- package/dist/boards/work.d.ts.map +1 -1
- package/dist/boards/work.js +70 -7
- package/dist/boards/work.js.map +1 -1
- package/dist/capabilities/version-control.d.ts +26 -0
- package/dist/capabilities/version-control.d.ts.map +1 -1
- package/dist/factory.d.ts +8 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +191 -16
- package/dist/factory.js.map +1 -1
- package/dist/integrations/github/integration.d.ts.map +1 -1
- package/dist/integrations/github/integration.js +37 -1
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/gitlab/api.d.ts +15 -0
- package/dist/integrations/gitlab/api.d.ts.map +1 -1
- package/dist/integrations/gitlab/api.js +9 -0
- package/dist/integrations/gitlab/api.js.map +1 -1
- package/dist/integrations/gitlab/version-control.d.ts.map +1 -1
- package/dist/integrations/gitlab/version-control.js +37 -3
- package/dist/integrations/gitlab/version-control.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +27 -1
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/subscription-session.d.ts +15 -0
- package/dist/integrations/subscription-session.d.ts.map +1 -1
- package/dist/integrations/subscription-session.js +30 -13
- package/dist/integrations/subscription-session.js.map +1 -1
- package/dist/mcp/disconnect.d.ts +10 -0
- package/dist/mcp/disconnect.d.ts.map +1 -0
- package/dist/mcp/disconnect.js +44 -0
- package/dist/mcp/disconnect.js.map +1 -0
- package/dist/mcp/routes.d.ts +46 -0
- package/dist/mcp/routes.d.ts.map +1 -0
- package/dist/mcp/routes.js +243 -0
- package/dist/mcp/routes.js.map +1 -0
- package/dist/provider-display-name.d.ts +2 -0
- package/dist/provider-display-name.d.ts.map +1 -0
- package/dist/provider-display-name.js +16 -0
- package/dist/provider-display-name.js.map +1 -0
- package/dist/routes/config.js +2 -2
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/contracts.d.ts +4 -0
- package/dist/routes/contracts.d.ts.map +1 -1
- package/dist/routes/contracts.js +3 -1
- package/dist/routes/contracts.js.map +1 -1
- package/dist/routes/oauth.js +1 -1
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/provider-credentials.d.ts +6 -2
- package/dist/routes/provider-credentials.d.ts.map +1 -1
- package/dist/routes/provider-credentials.js +20 -7
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/source-control-sessions.d.ts +1 -0
- package/dist/routes/source-control-sessions.d.ts.map +1 -1
- package/dist/routes/source-control-sessions.js +1 -1
- package/dist/routes/source-control-sessions.js.map +1 -1
- package/dist/routes/surface.d.ts +1 -0
- package/dist/routes/surface.d.ts.map +1 -1
- package/dist/routes/surface.js +4 -3
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/work-items.d.ts.map +1 -1
- package/dist/routes/work-items.js +61 -121
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/dispatcher.d.ts +7 -0
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +62 -5
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/processor.d.ts.map +1 -1
- package/dist/rules/processor.js +2 -1
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/tools.d.ts.map +1 -1
- package/dist/rules/tools.js +82 -3
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.d.ts +17 -0
- package/dist/rules/transition-service.d.ts.map +1 -1
- package/dist/rules/transition-service.js +25 -0
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.d.ts +2 -4
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/types.js +1 -4
- package/dist/rules/types.js.map +1 -1
- package/dist/sandbox/local-factory-sandbox.d.ts.map +1 -1
- package/dist/sandbox/local-factory-sandbox.js +1 -0
- package/dist/sandbox/local-factory-sandbox.js.map +1 -1
- package/dist/session/factory-session.d.ts +15 -1
- package/dist/session/factory-session.d.ts.map +1 -1
- package/dist/session/factory-session.js +4 -2
- package/dist/session/factory-session.js.map +1 -1
- package/dist/session/reply-credential-processor.d.ts +15 -0
- package/dist/session/reply-credential-processor.d.ts.map +1 -0
- package/dist/session/reply-credential-processor.js +40 -0
- package/dist/session/reply-credential-processor.js.map +1 -0
- package/dist/session/source-control-tools.d.ts +19 -1
- package/dist/session/source-control-tools.d.ts.map +1 -1
- package/dist/session/source-control-tools.js +70 -3
- package/dist/session/source-control-tools.js.map +1 -1
- package/dist/storage/domains/audit/domain.js +1 -1
- package/dist/storage/domains/projects/base.d.ts +6 -0
- package/dist/storage/domains/projects/base.d.ts.map +1 -1
- package/dist/storage/domains/projects/base.js +12 -0
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +17 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +23 -0
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +5 -1
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts +56 -0
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +139 -25
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/tools/annotations.d.ts +7 -0
- package/dist/tools/annotations.d.ts.map +1 -0
- package/dist/tools/annotations.js +102 -0
- package/dist/tools/annotations.js.map +1 -0
- package/dist/tools/approval-tools.d.ts +15 -0
- package/dist/tools/approval-tools.d.ts.map +1 -0
- package/dist/tools/approval-tools.js +77 -0
- package/dist/tools/approval-tools.js.map +1 -0
- package/dist/tools/deliver.d.ts +54 -0
- package/dist/tools/deliver.d.ts.map +1 -0
- package/dist/tools/deliver.js +105 -0
- package/dist/tools/deliver.js.map +1 -0
- package/dist/tools/index.d.ts +29 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +39 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/messaging-tools.d.ts +28 -0
- package/dist/tools/messaging-tools.d.ts.map +1 -0
- package/dist/tools/messaging-tools.js +312 -0
- package/dist/tools/messaging-tools.js.map +1 -0
- package/dist/tools/permissions.d.ts +31 -0
- package/dist/tools/permissions.d.ts.map +1 -0
- package/dist/tools/permissions.js +44 -0
- package/dist/tools/permissions.js.map +1 -0
- package/dist/tools/read-tools.d.ts +27 -0
- package/dist/tools/read-tools.d.ts.map +1 -0
- package/dist/tools/read-tools.js +285 -0
- package/dist/tools/read-tools.js.map +1 -0
- package/dist/tools/reports.d.ts +163 -0
- package/dist/tools/reports.d.ts.map +1 -0
- package/dist/tools/reports.js +352 -0
- package/dist/tools/reports.js.map +1 -0
- package/dist/tools/run-watch.d.ts +46 -0
- package/dist/tools/run-watch.d.ts.map +1 -0
- package/dist/tools/run-watch.js +102 -0
- package/dist/tools/run-watch.js.map +1 -0
- package/dist/tools/scope.d.ts +45 -0
- package/dist/tools/scope.d.ts.map +1 -0
- package/dist/tools/scope.js +73 -0
- package/dist/tools/scope.js.map +1 -0
- package/dist/tools/session-tools.d.ts +31 -0
- package/dist/tools/session-tools.d.ts.map +1 -0
- package/dist/tools/session-tools.js +312 -0
- package/dist/tools/session-tools.js.map +1 -0
- package/dist/tools/sessions-state-processor.d.ts +37 -0
- package/dist/tools/sessions-state-processor.d.ts.map +1 -0
- package/dist/tools/sessions-state-processor.js +206 -0
- package/dist/tools/sessions-state-processor.js.map +1 -0
- package/dist/tools/work-item-tools.d.ts +28 -0
- package/dist/tools/work-item-tools.d.ts.map +1 -0
- package/dist/tools/work-item-tools.js +322 -0
- package/dist/tools/work-item-tools.js.map +1 -0
- package/dist/work-items/create-work-item.d.ts +55 -0
- package/dist/work-items/create-work-item.d.ts.map +1 -0
- package/dist/work-items/create-work-item.js +101 -0
- package/dist/work-items/create-work-item.js.map +1 -0
- package/dist/work-items/decision-actions.d.ts +42 -0
- package/dist/work-items/decision-actions.d.ts.map +1 -0
- package/dist/work-items/decision-actions.js +70 -0
- package/dist/work-items/decision-actions.js.map +1 -0
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +3 -1
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-gitlab-rereview/SKILL.md +1 -1
- package/factory-skills/factory-gitlab-review/SKILL.md +1 -1
- package/factory-skills/factory-plan/SKILL.md +4 -4
- package/factory-skills/factory-rereview/SKILL.md +3 -1
- package/factory-skills/factory-review/SKILL.md +3 -1
- package/factory-skills/factory-self-review/SKILL.md +46 -0
- package/package.json +13 -11
|
@@ -53,6 +53,30 @@ async function prepareSessionRunContext(requestContext, sessionId, sourceControl
|
|
|
53
53
|
await primeTenantCredentialsForRequestContext(requestContext);
|
|
54
54
|
return "prepared";
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Create the controller session for a Factory session that isn't live, as its
|
|
58
|
+
* row's owner in the row's organization. Used when an event or message has to
|
|
59
|
+
* reach a session nobody has open: there is no signed-in user, so the session
|
|
60
|
+
* runs as the person it belongs to.
|
|
61
|
+
*/
|
|
62
|
+
async function createSessionAsRowOwner(controller, args) {
|
|
63
|
+
const requestContext = new RequestContext();
|
|
64
|
+
requestContext.set("user", {
|
|
65
|
+
workosId: args.row.userId,
|
|
66
|
+
organizationId: args.row.orgId
|
|
67
|
+
});
|
|
68
|
+
const session = await controller.createSession({
|
|
69
|
+
id: args.sessionId,
|
|
70
|
+
ownerId: args.row.userId,
|
|
71
|
+
resourceId: args.ownerResourceId,
|
|
72
|
+
scope: args.scope,
|
|
73
|
+
tags: args.tags,
|
|
74
|
+
...args.threadId ? { threadId: args.threadId } : {},
|
|
75
|
+
requestContext
|
|
76
|
+
});
|
|
77
|
+
await seedSessionOrg(session, args.row.orgId);
|
|
78
|
+
return session;
|
|
79
|
+
}
|
|
56
80
|
async function resolveSubscriptionSession(controller, subscription, options) {
|
|
57
81
|
const { label } = options;
|
|
58
82
|
const { sessionId, resourceId, threadId } = subscription;
|
|
@@ -69,20 +93,13 @@ async function resolveSubscriptionSession(controller, subscription, options) {
|
|
|
69
93
|
};
|
|
70
94
|
const sessionRow = await options.sourceControl?.sessions.getBySessionId(sessionId);
|
|
71
95
|
if (!sessionRow) throw new Error(`${label} subscription ${subscription.id} has no Factory session ${sessionId} to run as.`);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
});
|
|
77
|
-
session = await controller.createSession({
|
|
78
|
-
id: sessionId,
|
|
79
|
-
ownerId: sessionRow.userId,
|
|
80
|
-
resourceId: ownerResourceId,
|
|
96
|
+
session = await createSessionAsRowOwner(controller, {
|
|
97
|
+
sessionId,
|
|
98
|
+
ownerResourceId,
|
|
99
|
+
row: sessionRow,
|
|
81
100
|
scope,
|
|
82
|
-
tags
|
|
83
|
-
requestContext
|
|
101
|
+
tags
|
|
84
102
|
});
|
|
85
|
-
await seedSessionOrg(session, sessionRow.orgId);
|
|
86
103
|
} else if (!hasResolvedOrg(session.state?.get()?.factoryOrgId)) try {
|
|
87
104
|
const sessionRow = await options.sourceControl?.sessions.getBySessionId(sessionId);
|
|
88
105
|
await seedSessionOrg(session, sessionRow?.orgId);
|
|
@@ -99,6 +116,6 @@ async function resolveSubscriptionSession(controller, subscription, options) {
|
|
|
99
116
|
return session;
|
|
100
117
|
}
|
|
101
118
|
//#endregion
|
|
102
|
-
export { prepareSessionRunContext, resolveSubscriptionSession, subscriptionRunContext };
|
|
119
|
+
export { createSessionAsRowOwner, prepareSessionRunContext, resolveSubscriptionSession, subscriptionRunContext };
|
|
103
120
|
|
|
104
121
|
//# sourceMappingURL=subscription-session.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subscription-session.js","names":[],"sources":["../../src/integrations/subscription-session.ts"],"sourcesContent":["import type { MountedMastraCode } from '@mastra/code-sdk';\nimport { RequestContext } from '@mastra/core/request-context';\n\nimport { primeTenantCredentialsForRequestContext } from '../routes/tenant-credentials.js';\nimport { hasResolvedOrg, seedSessionOrg } from '../session/org-seed.js';\nimport type { IntegrationSubscription } from '../storage/domains/integrations/base.js';\n\n/** The Factory session row fields a woken session has to run as. */\nexport type FactorySessionOwner = { userId: string; orgId: string };\n\nexport interface SubscriptionSessionLookup {\n sessions: { getBySessionId(sessionId: string): Promise<FactorySessionOwner | null> };\n}\n\n/**\n * Bring the session that owns a subscribed thread online so a provider event\n * can be delivered to it. Provider-neutral: GitHub and GitLab subscriptions\n * carry the same session binding and differ only in their target data.\n */\nexport type SubscriptionSessionRow = IntegrationSubscription<{\n projectRepositoryId: string;\n subscribedByUserId?: string | null;\n}>;\n\n/**\n * The request context a provider-triggered run executes under: the user who\n * subscribed the thread, in the subscription's organization. A webhook carries\n * no signed-in user and tenant credential resolution fails closed without one,\n * so a woken run would otherwise stop before it could persist or answer the\n * notification. The subscription row already names that user, so delivery\n * costs no extra read; an older row without one falls back to the Factory\n * session's owner, the identity used when a session is recreated for delivery.\n */\nexport async function subscriptionRunContext(\n subscription: SubscriptionSessionRow,\n sourceControl: SubscriptionSessionLookup | undefined,\n): Promise<RequestContext | undefined> {\n let userId = subscription.data.subscribedByUserId ?? undefined;\n let orgId: string | undefined = subscription.orgId;\n if (!userId && subscription.sessionId && sourceControl) {\n const sessionRow = await sourceControl.sessions.getBySessionId(subscription.sessionId);\n userId = sessionRow?.userId;\n orgId = sessionRow?.orgId ?? orgId;\n }\n if (!userId || !hasResolvedOrg(orgId)) return undefined;\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: userId, organizationId: orgId });\n // The web auth middleware primes credential snapshots per request; a\n // webhook-triggered run has no request, so prime here or the model resolves\n // against an empty snapshot and the run fails closed before persisting. A\n // priming failure propagates: handing back an unprimed context would let the\n // caller accept the delivery, and retire a terminal subscription, for a run\n // that then fails without its credentials and cannot be redelivered.\n try {\n await primeTenantCredentialsForRequestContext(requestContext);\n } catch (error) {\n throw new Error(`Unable to prime tenant credentials for subscription ${subscription.id}; not delivered.`, {\n cause: error,\n });\n }\n return requestContext;\n}\n\n/**\n * Attach the Factory session's owner, in its organization, to the request\n * context of a run that has no inbound request (a notification or peer-signal\n * wake), and prime that tenant's credentials. The live session's `ownerId`\n * isn't used: sessions opened from the browser or Slack are owned by the\n * controller id, not a user. Leaves the context alone when `sessionId` names no\n * Factory session or its org is unresolved, so credential resolution fails\n * closed.\n */\nexport async function prepareSessionRunContext(\n requestContext: RequestContext,\n sessionId: string,\n sourceControl: SubscriptionSessionLookup,\n options?: { expectedOrgId?: string },\n): Promise<'prepared' | 'session-not-found' | 'organization-unresolved' | 'organization-mismatch'> {\n const sessionRow = await sourceControl.sessions.getBySessionId(sessionId);\n if (!sessionRow) return 'session-not-found';\n if (!hasResolvedOrg(sessionRow.orgId)) return 'organization-unresolved';\n if (options?.expectedOrgId !== undefined && sessionRow.orgId !== options.expectedOrgId) {\n return 'organization-mismatch';\n }\n requestContext.set('user', { workosId: sessionRow.userId, organizationId: sessionRow.orgId });\n await primeTenantCredentialsForRequestContext(requestContext);\n return 'prepared';\n}\n\nexport async function resolveSubscriptionSession(\n controller: MountedMastraCode['controller'],\n subscription: SubscriptionSessionRow,\n options: { label: string; sourceControl?: SubscriptionSessionLookup },\n) {\n const { label } = options;\n const { sessionId, resourceId, threadId } = subscription;\n if (!sessionId || !resourceId || !threadId) {\n throw new Error(`${label} subscription ${subscription.id} is missing its session binding.`);\n }\n // Read the thread straight from storage before touching sessions. This answers\n // two questions at once, and `queryThreadById` does it without constructing a\n // session (so no workspace or sandbox is provisioned just to make the check).\n //\n // First: do we even have this thread? A change request's events can reach a\n // deployment that never owned the subscribed thread, and delivery must not\n // fabricate a session for a thread that lives somewhere else.\n //\n // Second: which resource owns it? The subscription records the Factory project\n // as its `resourceId`, but an unscoped session is registered under its own id,\n // so the stored value routinely names a resource that does not own the thread.\n // The thread row is the authoritative answer; the stored id is only a fallback.\n const thread = await controller.queryThreadById({ threadId });\n if (!thread) return undefined;\n const ownerResourceId = thread.resourceId || resourceId;\n const scope = subscription.sessionScope || undefined;\n let session = await controller.getSessionByResource(ownerResourceId, scope);\n if (!session) {\n const tags = {\n factoryProjectId: resourceId,\n projectRepositoryId: subscription.data.projectRepositoryId,\n };\n // Creating the session resolves its workspace, which authorizes the caller\n // against the Factory session row — no signed-in user, so run as its owner.\n // The session is created under the resource that owns the thread, so the\n // thread switch below resolves; the persisted Factory session is keyed by\n // the subscription's session ID.\n const sessionRow = await options.sourceControl?.sessions.getBySessionId(sessionId);\n if (!sessionRow) {\n throw new Error(`${label} subscription ${subscription.id} has no Factory session ${sessionId} to run as.`);\n }\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: sessionRow.userId, organizationId: sessionRow.orgId });\n session = await controller.createSession({\n id: sessionId,\n ownerId: sessionRow.userId,\n resourceId: ownerResourceId,\n scope,\n tags,\n requestContext,\n });\n await seedSessionOrg(session, sessionRow.orgId);\n } else if (!hasResolvedOrg(session.state?.get()?.factoryOrgId)) {\n // A session created before the org seed existed carries the project tag and\n // no org, so capture would refuse for the rest of its life even though the\n // org is recoverable. Heal it — but only here, and only when it is missing:\n // hoisting the row fetch above would make an existing-session delivery throw\n // on a missing row where it previously succeeded, and fetching it every time\n // would add a storage read to every delivery. A row that is missing or a\n // lookup that throws leaves the session marked unresolved, and delivery\n // continues either way.\n try {\n const sessionRow = await options.sourceControl?.sessions.getBySessionId(sessionId);\n await seedSessionOrg(session, sessionRow?.orgId);\n } catch (error) {\n console.warn(`[${label} webhook] Unable to resolve the session organization.`, error);\n await seedSessionOrg(session, undefined);\n }\n } else if (session.state?.get()?.factoryOrgUnresolved) {\n // The org is present, so an earlier failed resolution left a stale marker\n // behind. Clear it without a storage read — nothing else re-seeds a session\n // once the start hook has run, so the marker would otherwise outlive its\n // cause.\n await seedSessionOrg(session, session.state.get()?.factoryOrgId);\n }\n if (session.thread.getId() !== threadId) {\n await session.thread.switch({ threadId, emitEvent: false });\n }\n if (session.thread.getId() !== threadId) {\n throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);\n }\n return session;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"subscription-session.js","names":[],"sources":["../../src/integrations/subscription-session.ts"],"sourcesContent":["import type { MountedMastraCode } from '@mastra/code-sdk';\nimport { RequestContext } from '@mastra/core/request-context';\n\nimport { primeTenantCredentialsForRequestContext } from '../routes/tenant-credentials.js';\nimport { hasResolvedOrg, seedSessionOrg } from '../session/org-seed.js';\nimport type { IntegrationSubscription } from '../storage/domains/integrations/base.js';\n\n/** The Factory session row fields a woken session has to run as. */\nexport type FactorySessionOwner = { userId: string; orgId: string };\n\nexport interface SubscriptionSessionLookup {\n sessions: { getBySessionId(sessionId: string): Promise<FactorySessionOwner | null> };\n}\n\n/**\n * Bring the session that owns a subscribed thread online so a provider event\n * can be delivered to it. Provider-neutral: GitHub and GitLab subscriptions\n * carry the same session binding and differ only in their target data.\n */\nexport type SubscriptionSessionRow = IntegrationSubscription<{\n projectRepositoryId: string;\n subscribedByUserId?: string | null;\n}>;\n\n/**\n * The request context a provider-triggered run executes under: the user who\n * subscribed the thread, in the subscription's organization. A webhook carries\n * no signed-in user and tenant credential resolution fails closed without one,\n * so a woken run would otherwise stop before it could persist or answer the\n * notification. The subscription row already names that user, so delivery\n * costs no extra read; an older row without one falls back to the Factory\n * session's owner, the identity used when a session is recreated for delivery.\n */\nexport async function subscriptionRunContext(\n subscription: SubscriptionSessionRow,\n sourceControl: SubscriptionSessionLookup | undefined,\n): Promise<RequestContext | undefined> {\n let userId = subscription.data.subscribedByUserId ?? undefined;\n let orgId: string | undefined = subscription.orgId;\n if (!userId && subscription.sessionId && sourceControl) {\n const sessionRow = await sourceControl.sessions.getBySessionId(subscription.sessionId);\n userId = sessionRow?.userId;\n orgId = sessionRow?.orgId ?? orgId;\n }\n if (!userId || !hasResolvedOrg(orgId)) return undefined;\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: userId, organizationId: orgId });\n // The web auth middleware primes credential snapshots per request; a\n // webhook-triggered run has no request, so prime here or the model resolves\n // against an empty snapshot and the run fails closed before persisting. A\n // priming failure propagates: handing back an unprimed context would let the\n // caller accept the delivery, and retire a terminal subscription, for a run\n // that then fails without its credentials and cannot be redelivered.\n try {\n await primeTenantCredentialsForRequestContext(requestContext);\n } catch (error) {\n throw new Error(`Unable to prime tenant credentials for subscription ${subscription.id}; not delivered.`, {\n cause: error,\n });\n }\n return requestContext;\n}\n\n/**\n * Attach the Factory session's owner, in its organization, to the request\n * context of a run that has no inbound request (a notification or peer-signal\n * wake), and prime that tenant's credentials. The live session's `ownerId`\n * isn't used: sessions opened from the browser or Slack are owned by the\n * controller id, not a user. Leaves the context alone when `sessionId` names no\n * Factory session or its org is unresolved, so credential resolution fails\n * closed.\n */\nexport async function prepareSessionRunContext(\n requestContext: RequestContext,\n sessionId: string,\n sourceControl: SubscriptionSessionLookup,\n options?: { expectedOrgId?: string },\n): Promise<'prepared' | 'session-not-found' | 'organization-unresolved' | 'organization-mismatch'> {\n const sessionRow = await sourceControl.sessions.getBySessionId(sessionId);\n if (!sessionRow) return 'session-not-found';\n if (!hasResolvedOrg(sessionRow.orgId)) return 'organization-unresolved';\n if (options?.expectedOrgId !== undefined && sessionRow.orgId !== options.expectedOrgId) {\n return 'organization-mismatch';\n }\n requestContext.set('user', { workosId: sessionRow.userId, organizationId: sessionRow.orgId });\n await primeTenantCredentialsForRequestContext(requestContext);\n return 'prepared';\n}\n\n/**\n * Create the controller session for a Factory session that isn't live, as its\n * row's owner in the row's organization. Used when an event or message has to\n * reach a session nobody has open: there is no signed-in user, so the session\n * runs as the person it belongs to.\n */\nexport async function createSessionAsRowOwner(\n controller: Pick<MountedMastraCode['controller'], 'createSession'>,\n args: {\n sessionId: string;\n ownerResourceId: string;\n row: FactorySessionOwner;\n tags: Record<string, string>;\n scope?: string;\n /** Bind this exact thread at creation instead of resuming or creating another. */\n threadId?: string;\n },\n) {\n const requestContext = new RequestContext();\n requestContext.set('user', { workosId: args.row.userId, organizationId: args.row.orgId });\n const session = await controller.createSession({\n id: args.sessionId,\n ownerId: args.row.userId,\n resourceId: args.ownerResourceId,\n scope: args.scope,\n tags: args.tags,\n ...(args.threadId ? { threadId: args.threadId } : {}),\n requestContext,\n });\n await seedSessionOrg(session, args.row.orgId);\n return session;\n}\n\nexport async function resolveSubscriptionSession(\n controller: MountedMastraCode['controller'],\n subscription: SubscriptionSessionRow,\n options: { label: string; sourceControl?: SubscriptionSessionLookup },\n) {\n const { label } = options;\n const { sessionId, resourceId, threadId } = subscription;\n if (!sessionId || !resourceId || !threadId) {\n throw new Error(`${label} subscription ${subscription.id} is missing its session binding.`);\n }\n // Read the thread straight from storage before touching sessions. This answers\n // two questions at once, and `queryThreadById` does it without constructing a\n // session (so no workspace or sandbox is provisioned just to make the check).\n //\n // First: do we even have this thread? A change request's events can reach a\n // deployment that never owned the subscribed thread, and delivery must not\n // fabricate a session for a thread that lives somewhere else.\n //\n // Second: which resource owns it? The subscription records the Factory project\n // as its `resourceId`, but an unscoped session is registered under its own id,\n // so the stored value routinely names a resource that does not own the thread.\n // The thread row is the authoritative answer; the stored id is only a fallback.\n const thread = await controller.queryThreadById({ threadId });\n if (!thread) return undefined;\n const ownerResourceId = thread.resourceId || resourceId;\n const scope = subscription.sessionScope || undefined;\n let session = await controller.getSessionByResource(ownerResourceId, scope);\n if (!session) {\n const tags = {\n factoryProjectId: resourceId,\n projectRepositoryId: subscription.data.projectRepositoryId,\n };\n // Creating the session resolves its workspace, which authorizes the caller\n // against the Factory session row — no signed-in user, so run as its owner.\n // The session is created under the resource that owns the thread, so the\n // thread switch below resolves; the persisted Factory session is keyed by\n // the subscription's session ID.\n const sessionRow = await options.sourceControl?.sessions.getBySessionId(sessionId);\n if (!sessionRow) {\n throw new Error(`${label} subscription ${subscription.id} has no Factory session ${sessionId} to run as.`);\n }\n session = await createSessionAsRowOwner(controller, {\n sessionId,\n ownerResourceId,\n row: sessionRow,\n scope,\n tags,\n });\n } else if (!hasResolvedOrg(session.state?.get()?.factoryOrgId)) {\n // A session created before the org seed existed carries the project tag and\n // no org, so capture would refuse for the rest of its life even though the\n // org is recoverable. Heal it — but only here, and only when it is missing:\n // hoisting the row fetch above would make an existing-session delivery throw\n // on a missing row where it previously succeeded, and fetching it every time\n // would add a storage read to every delivery. A row that is missing or a\n // lookup that throws leaves the session marked unresolved, and delivery\n // continues either way.\n try {\n const sessionRow = await options.sourceControl?.sessions.getBySessionId(sessionId);\n await seedSessionOrg(session, sessionRow?.orgId);\n } catch (error) {\n console.warn(`[${label} webhook] Unable to resolve the session organization.`, error);\n await seedSessionOrg(session, undefined);\n }\n } else if (session.state?.get()?.factoryOrgUnresolved) {\n // The org is present, so an earlier failed resolution left a stale marker\n // behind. Clear it without a storage read — nothing else re-seeds a session\n // once the start hook has run, so the marker would otherwise outlive its\n // cause.\n await seedSessionOrg(session, session.state.get()?.factoryOrgId);\n }\n if (session.thread.getId() !== threadId) {\n await session.thread.switch({ threadId, emitEvent: false });\n }\n if (session.thread.getId() !== threadId) {\n throw new Error(`Session ${sessionId} did not bind thread ${threadId}.`);\n }\n return session;\n}\n"],"mappings":";;;;;;;;;;;;;AAiCA,eAAsB,uBACpB,cACA,eACqC;CACrC,IAAI,SAAS,aAAa,KAAK,sBAAsB,KAAA;CACrD,IAAI,QAA4B,aAAa;CAC7C,IAAI,CAAC,UAAU,aAAa,aAAa,eAAe;EACtD,MAAM,aAAa,MAAM,cAAc,SAAS,eAAe,aAAa,SAAS;EACrF,SAAS,YAAY;EACrB,QAAQ,YAAY,SAAS;CAC/B;CACA,IAAI,CAAC,UAAU,CAAC,eAAe,KAAK,GAAG,OAAO,KAAA;CAC9C,MAAM,iBAAiB,IAAI,eAAe;CAC1C,eAAe,IAAI,QAAQ;EAAE,UAAU;EAAQ,gBAAgB;CAAM,CAAC;CAOtE,IAAI;EACF,MAAM,wCAAwC,cAAc;CAC9D,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,uDAAuD,aAAa,GAAG,mBAAmB,EACxG,OAAO,MACT,CAAC;CACH;CACA,OAAO;AACT;;;;;;;;;;AAWA,eAAsB,yBACpB,gBACA,WACA,eACA,SACiG;CACjG,MAAM,aAAa,MAAM,cAAc,SAAS,eAAe,SAAS;CACxE,IAAI,CAAC,YAAY,OAAO;CACxB,IAAI,CAAC,eAAe,WAAW,KAAK,GAAG,OAAO;CAC9C,IAAI,SAAS,kBAAkB,KAAA,KAAa,WAAW,UAAU,QAAQ,eACvE,OAAO;CAET,eAAe,IAAI,QAAQ;EAAE,UAAU,WAAW;EAAQ,gBAAgB,WAAW;CAAM,CAAC;CAC5F,MAAM,wCAAwC,cAAc;CAC5D,OAAO;AACT;;;;;;;AAQA,eAAsB,wBACpB,YACA,MASA;CACA,MAAM,iBAAiB,IAAI,eAAe;CAC1C,eAAe,IAAI,QAAQ;EAAE,UAAU,KAAK,IAAI;EAAQ,gBAAgB,KAAK,IAAI;CAAM,CAAC;CACxF,MAAM,UAAU,MAAM,WAAW,cAAc;EAC7C,IAAI,KAAK;EACT,SAAS,KAAK,IAAI;EAClB,YAAY,KAAK;EACjB,OAAO,KAAK;EACZ,MAAM,KAAK;EACX,GAAI,KAAK,WAAW,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;EACnD;CACF,CAAC;CACD,MAAM,eAAe,SAAS,KAAK,IAAI,KAAK;CAC5C,OAAO;AACT;AAEA,eAAsB,2BACpB,YACA,cACA,SACA;CACA,MAAM,EAAE,UAAU;CAClB,MAAM,EAAE,WAAW,YAAY,aAAa;CAC5C,IAAI,CAAC,aAAa,CAAC,cAAc,CAAC,UAChC,MAAM,IAAI,MAAM,GAAG,MAAM,gBAAgB,aAAa,GAAG,iCAAiC;CAc5F,MAAM,SAAS,MAAM,WAAW,gBAAgB,EAAE,SAAS,CAAC;CAC5D,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,MAAM,kBAAkB,OAAO,cAAc;CAC7C,MAAM,QAAQ,aAAa,gBAAgB,KAAA;CAC3C,IAAI,UAAU,MAAM,WAAW,qBAAqB,iBAAiB,KAAK;CAC1E,IAAI,CAAC,SAAS;EACZ,MAAM,OAAO;GACX,kBAAkB;GAClB,qBAAqB,aAAa,KAAK;EACzC;EAMA,MAAM,aAAa,MAAM,QAAQ,eAAe,SAAS,eAAe,SAAS;EACjF,IAAI,CAAC,YACH,MAAM,IAAI,MAAM,GAAG,MAAM,gBAAgB,aAAa,GAAG,0BAA0B,UAAU,YAAY;EAE3G,UAAU,MAAM,wBAAwB,YAAY;GAClD;GACA;GACA,KAAK;GACL;GACA;EACF,CAAC;CACH,OAAO,IAAI,CAAC,eAAe,QAAQ,OAAO,IAAI,CAAC,EAAE,YAAY,GAS3D,IAAI;EACF,MAAM,aAAa,MAAM,QAAQ,eAAe,SAAS,eAAe,SAAS;EACjF,MAAM,eAAe,SAAS,YAAY,KAAK;CACjD,SAAS,OAAO;EACd,QAAQ,KAAK,IAAI,MAAM,wDAAwD,KAAK;EACpF,MAAM,eAAe,SAAS,KAAA,CAAS;CACzC;MACK,IAAI,QAAQ,OAAO,IAAI,CAAC,EAAE,sBAK/B,MAAM,eAAe,SAAS,QAAQ,MAAM,IAAI,CAAC,EAAE,YAAY;CAEjE,IAAI,QAAQ,OAAO,MAAM,MAAM,UAC7B,MAAM,QAAQ,OAAO,OAAO;EAAE;EAAU,WAAW;CAAM,CAAC;CAE5D,IAAI,QAAQ,OAAO,MAAM,MAAM,UAC7B,MAAM,IAAI,MAAM,WAAW,UAAU,uBAAuB,SAAS,EAAE;CAEzE,OAAO;AACT"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emits `close` on the `toReqRes` response when the HTTP client cancels the
|
|
3
|
+
* body, so the MCP transport aborts instead of writing into a closed stream.
|
|
4
|
+
* Mirrors `@mastra/hono`'s helper, which isn't exported.
|
|
5
|
+
*/
|
|
6
|
+
export declare function propagateClientDisconnect(fetchResponse: Response, res: {
|
|
7
|
+
emit: (event: string) => void;
|
|
8
|
+
destroy?: () => void;
|
|
9
|
+
}): Response;
|
|
10
|
+
//# sourceMappingURL=disconnect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disconnect.d.ts","sourceRoot":"","sources":["../../src/mcp/disconnect.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,aAAa,EAAE,QAAQ,EACvB,GAAG,EAAE;IAAE,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,GAC3D,QAAQ,CA+CV"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region src/mcp/disconnect.ts
|
|
2
|
+
/**
|
|
3
|
+
* Emits `close` on the `toReqRes` response when the HTTP client cancels the
|
|
4
|
+
* body, so the MCP transport aborts instead of writing into a closed stream.
|
|
5
|
+
* Mirrors `@mastra/hono`'s helper, which isn't exported.
|
|
6
|
+
*/
|
|
7
|
+
function propagateClientDisconnect(fetchResponse, res) {
|
|
8
|
+
const upstream = fetchResponse.body;
|
|
9
|
+
if (!upstream) return fetchResponse;
|
|
10
|
+
let disconnected = false;
|
|
11
|
+
const disconnect = () => {
|
|
12
|
+
if (disconnected) return;
|
|
13
|
+
disconnected = true;
|
|
14
|
+
try {
|
|
15
|
+
res.emit("close");
|
|
16
|
+
} catch {}
|
|
17
|
+
reader.read().then(function drain({ done }) {
|
|
18
|
+
return done ? void 0 : reader.read().then(drain);
|
|
19
|
+
}, () => {});
|
|
20
|
+
};
|
|
21
|
+
const reader = upstream.getReader();
|
|
22
|
+
const body = new ReadableStream({
|
|
23
|
+
async pull(controller) {
|
|
24
|
+
const { done, value } = await reader.read();
|
|
25
|
+
if (done) {
|
|
26
|
+
controller.close();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
controller.enqueue(value);
|
|
30
|
+
},
|
|
31
|
+
cancel() {
|
|
32
|
+
disconnect();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
return new Response(body, {
|
|
36
|
+
status: fetchResponse.status,
|
|
37
|
+
statusText: fetchResponse.statusText,
|
|
38
|
+
headers: fetchResponse.headers
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { propagateClientDisconnect };
|
|
43
|
+
|
|
44
|
+
//# sourceMappingURL=disconnect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disconnect.js","names":[],"sources":["../../src/mcp/disconnect.ts"],"sourcesContent":["/**\n * Emits `close` on the `toReqRes` response when the HTTP client cancels the\n * body, so the MCP transport aborts instead of writing into a closed stream.\n * Mirrors `@mastra/hono`'s helper, which isn't exported.\n */\nexport function propagateClientDisconnect(\n fetchResponse: Response,\n res: { emit: (event: string) => void; destroy?: () => void },\n): Response {\n const upstream = fetchResponse.body;\n if (!upstream) return fetchResponse;\n\n let disconnected = false;\n const disconnect = () => {\n if (disconnected) return;\n disconnected = true;\n try {\n res.emit('close');\n } catch {\n // Already torn down - the transport has nothing left to clean up.\n }\n // Deliberately *not* cancelling or destroying the bridge stream here. `fetch-to-node`\n // buffers writes and flushes them from a cork timer; tearing its controller down leaves\n // that pending flush to enqueue into a closed controller, which throws an unhandled\n // ERR_INVALID_STATE from a timer callback - the very crash this guards against.\n // Emitting `close` aborts the transport, which ends the response, so the bridge closes\n // its own controller in the right order once buffered data has drained.\n void reader.read().then(\n function drain({ done }): unknown {\n return done ? undefined : reader.read().then(drain);\n },\n () => {},\n );\n };\n\n const reader = upstream.getReader();\n const body = new ReadableStream<Uint8Array>({\n async pull(controller) {\n const { done, value } = await reader.read();\n if (done) {\n controller.close();\n return;\n }\n controller.enqueue(value);\n },\n cancel() {\n disconnect();\n },\n });\n\n return new Response(body, {\n status: fetchResponse.status,\n statusText: fetchResponse.statusText,\n headers: fetchResponse.headers,\n });\n}\n"],"mappings":";;;;;;AAKA,SAAgB,0BACd,eACA,KACU;CACV,MAAM,WAAW,cAAc;CAC/B,IAAI,CAAC,UAAU,OAAO;CAEtB,IAAI,eAAe;CACnB,MAAM,mBAAmB;EACvB,IAAI,cAAc;EAClB,eAAe;EACf,IAAI;GACF,IAAI,KAAK,OAAO;EAClB,QAAQ,CAER;EAOA,OAAY,KAAK,CAAC,CAAC,KACjB,SAAS,MAAM,EAAE,QAAiB;GAChC,OAAO,OAAO,KAAA,IAAY,OAAO,KAAK,CAAC,CAAC,KAAK,KAAK;EACpD,SACM,CAAC,CACT;CACF;CAEA,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,OAAO,IAAI,eAA2B;EAC1C,MAAM,KAAK,YAAY;GACrB,MAAM,EAAE,MAAM,UAAU,MAAM,OAAO,KAAK;GAC1C,IAAI,MAAM;IACR,WAAW,MAAM;IACjB;GACF;GACA,WAAW,QAAQ,KAAK;EAC1B;EACA,SAAS;GACP,WAAW;EACb;CACF,CAAC;CAED,OAAO,IAAI,SAAS,MAAM;EACxB,QAAQ,cAAc;EACtB,YAAY,cAAc;EAC1B,SAAS,cAAc;CACzB,CAAC;AACH"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ApiRoute } from '@mastra/core/server';
|
|
2
|
+
import type { Tool } from '@mastra/core/tools';
|
|
3
|
+
import type { RouteAuth } from '../routes/route.js';
|
|
4
|
+
import type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';
|
|
5
|
+
import type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';
|
|
6
|
+
import type { FactoryToolDependencies, FactoryToolScope } from '../tools/index.js';
|
|
7
|
+
export declare const FACTORY_MCP_PATH = "/web/factory/projects/:projectId/mcp";
|
|
8
|
+
export interface FactoryMcpConfig {
|
|
9
|
+
/** Serve the MCP endpoint. Default `true`; it is never served without an auth provider. */
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* OAuth authorization servers advertised in the protected-resource metadata
|
|
13
|
+
* (RFC 9728) so clients like ChatGPT know where to sign in. Defaults to the
|
|
14
|
+
* `MASTRA_FACTORY_MCP_AUTHORIZATION_SERVER` env var, then to Mastra Platform's
|
|
15
|
+
* AuthKit when the Factory uses Platform auth. The endpoint isn't served
|
|
16
|
+
* without one, since clients would have nowhere to sign in.
|
|
17
|
+
*/
|
|
18
|
+
authorizationServers?: string[];
|
|
19
|
+
}
|
|
20
|
+
/** Mastra Platform's WorkOS AuthKit, which issues tokens Platform's `/auth/verify` accepts. */
|
|
21
|
+
export declare const PLATFORM_MCP_AUTHORIZATION_SERVER = "https://auth-kit.mastra.ai";
|
|
22
|
+
export declare function resolveMcpAuthorizationServers(providerName: string | undefined, config: FactoryMcpConfig | undefined, env?: Record<string, string | undefined>): string[];
|
|
23
|
+
export interface FactoryMcpRoutesDeps {
|
|
24
|
+
auth: RouteAuth;
|
|
25
|
+
publicOrigin: string;
|
|
26
|
+
authorizationServers: string[];
|
|
27
|
+
projects: Pick<FactoryProjectsStorage, 'get'>;
|
|
28
|
+
/** Source-control handles a connector session can be filed under, tried in order. */
|
|
29
|
+
sourceControls: readonly SourceControlStorageHandle[];
|
|
30
|
+
tools: FactoryToolDependencies;
|
|
31
|
+
version?: string;
|
|
32
|
+
}
|
|
33
|
+
/** The connector session a user's MCP client acts from: one per user and Factory, stable across requests. */
|
|
34
|
+
export declare function connectorSessionId(orgId: string, userId: string, factoryProjectId: string): string;
|
|
35
|
+
/** The tools a signed-in person's MCP client sees for one Factory. */
|
|
36
|
+
export declare function createFactoryMcpTools(scope: FactoryToolScope, deps: FactoryMcpRoutesDeps, options?: {
|
|
37
|
+
persistedSession?: boolean;
|
|
38
|
+
}): Record<string, Tool<any, any, any, any>>;
|
|
39
|
+
/**
|
|
40
|
+
* The Factory's MCP endpoint: Streamable HTTP at
|
|
41
|
+
* `/web/factory/projects/:projectId/mcp`, plus its RFC 9728 protected-resource
|
|
42
|
+
* metadata. Callers sign in with OAuth through the Factory's auth provider and
|
|
43
|
+
* get the tools a web UI session would, under the same permissions.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildFactoryMcpRoutes(deps: FactoryMcpRoutesDeps): ApiRoute[];
|
|
46
|
+
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/mcp/routes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAGpD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAQ/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAGpD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qCAAqC,CAAC;AAClF,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,2CAA2C,CAAC;AAG5F,OAAO,KAAK,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAGnF,eAAO,MAAM,gBAAgB,yCAAyC,CAAC;AASvE,MAAM,WAAW,gBAAgB;IAC/B,2FAA2F;IAC3F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,+FAA+F;AAC/F,eAAO,MAAM,iCAAiC,+BAA+B,CAAC;AAE9E,wBAAgB,8BAA8B,CAC5C,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,MAAM,EAAE,gBAAgB,GAAG,SAAS,EACpC,GAAG,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAe,GACpD,MAAM,EAAE,CAMV;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,SAAS,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,QAAQ,EAAE,IAAI,CAAC,sBAAsB,EAAE,KAAK,CAAC,CAAC;IAC9C,qFAAqF;IACrF,cAAc,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACtD,KAAK,EAAE,uBAAuB,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6GAA6G;AAC7G,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,MAAM,CAGlG;AAyFD,sEAAsE;AACtE,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,gBAAgB,EACvB,IAAI,EAAE,oBAAoB,EAC1B,OAAO,GAAE;IAAE,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAO,GAC3C,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAe1C;AAwED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,oBAAoB,GAAG,QAAQ,EAAE,CAoB5E"}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import "../platform-auth-host.js";
|
|
2
|
+
import { getFactoryAuthUser } from "../auth.js";
|
|
3
|
+
import { FactorySourceSessionResolutionError, ensureFactorySourceSession } from "../session/factory-session.js";
|
|
4
|
+
import { auditActorProfile } from "../storage/domains/audit/base.js";
|
|
5
|
+
import { FACTORY_TOOL_ANNOTATIONS } from "../tools/annotations.js";
|
|
6
|
+
import { createFactoryTools } from "../tools/index.js";
|
|
7
|
+
import { propagateClientDisconnect } from "./disconnect.js";
|
|
8
|
+
import { registerApiRoute } from "@mastra/core/server";
|
|
9
|
+
import { createHash } from "crypto";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { createTool } from "@mastra/core/tools";
|
|
12
|
+
import { MCPServer } from "@mastra/mcp";
|
|
13
|
+
import { toFetchResponse, toReqRes } from "fetch-to-node";
|
|
14
|
+
//#region src/mcp/routes.ts
|
|
15
|
+
const FACTORY_MCP_PATH = "/web/factory/projects/:projectId/mcp";
|
|
16
|
+
const PROTECTED_RESOURCE_PREFIX = "/.well-known/oauth-protected-resource";
|
|
17
|
+
/**
|
|
18
|
+
* Tools an MCP client never gets. `factory_message_origin` answers the session
|
|
19
|
+
* that started this one; a connector session is started by a person, not a session.
|
|
20
|
+
*/
|
|
21
|
+
const EXCLUDED_TOOLS = /* @__PURE__ */ new Set(["factory_message_origin"]);
|
|
22
|
+
/** Mastra Platform's WorkOS AuthKit, which issues tokens Platform's `/auth/verify` accepts. */
|
|
23
|
+
const PLATFORM_MCP_AUTHORIZATION_SERVER = "https://auth-kit.mastra.ai";
|
|
24
|
+
function resolveMcpAuthorizationServers(providerName, config, env = process.env) {
|
|
25
|
+
if (config?.authorizationServers?.length) return config.authorizationServers;
|
|
26
|
+
const fromEnv = env.MASTRA_FACTORY_MCP_AUTHORIZATION_SERVER?.trim();
|
|
27
|
+
if (fromEnv) return [fromEnv];
|
|
28
|
+
if (providerName === "mastra-studio") return [PLATFORM_MCP_AUTHORIZATION_SERVER];
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
/** The connector session a user's MCP client acts from: one per user and Factory, stable across requests. */
|
|
32
|
+
function connectorSessionId(orgId, userId, factoryProjectId) {
|
|
33
|
+
const hex = createHash("sha256").update(`mcp\0${orgId}\0${userId}\0${factoryProjectId}`).digest("hex");
|
|
34
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Get or create the private user session behind a person's MCP client, so
|
|
38
|
+
* session-scoped tools (start/signal sessions, follow work items) have a real
|
|
39
|
+
* session to act from. A Factory without a connected repository has nowhere to
|
|
40
|
+
* file one; its tools still run against the stable id.
|
|
41
|
+
*/
|
|
42
|
+
async function ensureConnectorSession(deps, args) {
|
|
43
|
+
const sessionId = connectorSessionId(args.orgId, args.userId, args.factoryProjectId);
|
|
44
|
+
if (await deps.tools.sessions.getBySessionId(sessionId)) return {
|
|
45
|
+
sessionId,
|
|
46
|
+
persisted: true
|
|
47
|
+
};
|
|
48
|
+
for (const sourceControl of deps.sourceControls) try {
|
|
49
|
+
await ensureFactorySourceSession({
|
|
50
|
+
sourceControl,
|
|
51
|
+
orgId: args.orgId,
|
|
52
|
+
factoryProjectId: args.factoryProjectId,
|
|
53
|
+
branch: `factory/mcp-${sessionId.slice(0, 8)}`,
|
|
54
|
+
attributeToUserId: args.userId,
|
|
55
|
+
sessionId,
|
|
56
|
+
visibility: "private"
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
sessionId,
|
|
60
|
+
persisted: true
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (!(error instanceof FactorySourceSessionResolutionError)) throw error;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
sessionId,
|
|
67
|
+
persisted: false
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function workItemUrl(publicOrigin, factoryProjectId, workItemId) {
|
|
71
|
+
return `${publicOrigin}/factory/${encodeURIComponent(factoryProjectId)}/work-items/${encodeURIComponent(workItemId)}`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* `search` and `fetch`, the two tools ChatGPT's connectors and deep research
|
|
75
|
+
* look for, answered from the Factory's work items.
|
|
76
|
+
*/
|
|
77
|
+
function createSearchFetchTools(scope, deps, inspect) {
|
|
78
|
+
const project = {
|
|
79
|
+
orgId: scope.orgId,
|
|
80
|
+
factoryProjectId: scope.factoryProjectId
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
search: createTool({
|
|
84
|
+
id: "search",
|
|
85
|
+
description: "Search this Factory’s work items by title or id. Returns matching items with ids to pass to `fetch`.",
|
|
86
|
+
inputSchema: z.object({ query: z.string().describe("Words to look for in work item titles.") }),
|
|
87
|
+
execute: async ({ query }) => {
|
|
88
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
89
|
+
return { results: (await deps.tools.workItems.list(project)).filter((item) => {
|
|
90
|
+
const haystack = `${item.id} ${item.title}`.toLowerCase();
|
|
91
|
+
return terms.every((term) => haystack.includes(term));
|
|
92
|
+
}).slice(0, 20).map((item) => ({
|
|
93
|
+
id: item.id,
|
|
94
|
+
title: item.title,
|
|
95
|
+
url: workItemUrl(deps.publicOrigin, scope.factoryProjectId, item.id)
|
|
96
|
+
})) };
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
fetch: createTool({
|
|
100
|
+
id: "fetch",
|
|
101
|
+
description: "Fetch everything the Factory knows about one work item, by the id `search` returned.",
|
|
102
|
+
inputSchema: z.object({ id: z.string().describe("Work item id.") }),
|
|
103
|
+
execute: async ({ id }, context) => {
|
|
104
|
+
if (!inspect?.execute) throw new Error("Work item details are unavailable.");
|
|
105
|
+
const details = await inspect.execute({ workItem: id }, context);
|
|
106
|
+
return {
|
|
107
|
+
id,
|
|
108
|
+
title: details.item?.title ?? id,
|
|
109
|
+
text: JSON.stringify(details, null, 2),
|
|
110
|
+
url: workItemUrl(deps.publicOrigin, scope.factoryProjectId, id),
|
|
111
|
+
metadata: { factoryProjectId: scope.factoryProjectId }
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** The tools a signed-in person's MCP client sees for one Factory. */
|
|
118
|
+
function createFactoryMcpTools(scope, deps, options = {}) {
|
|
119
|
+
const factoryTools = createFactoryTools({
|
|
120
|
+
scope,
|
|
121
|
+
deps: deps.tools
|
|
122
|
+
});
|
|
123
|
+
const tools = {};
|
|
124
|
+
for (const [name, tool] of Object.entries(factoryTools)) {
|
|
125
|
+
if (EXCLUDED_TOOLS.has(name)) continue;
|
|
126
|
+
if (name === "factory_start_session" && options.persistedSession === false) continue;
|
|
127
|
+
tools[name] = tool;
|
|
128
|
+
}
|
|
129
|
+
Object.assign(tools, createSearchFetchTools(scope, deps, factoryTools.factory_inspect_work_item));
|
|
130
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
131
|
+
const annotations = FACTORY_TOOL_ANNOTATIONS[name];
|
|
132
|
+
if (annotations) tool.mcp = {
|
|
133
|
+
...tool.mcp,
|
|
134
|
+
annotations
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return tools;
|
|
138
|
+
}
|
|
139
|
+
function resourceMetadataUrl(deps, mcpPath) {
|
|
140
|
+
return `${deps.publicOrigin}${PROTECTED_RESOURCE_PREFIX}${mcpPath}`;
|
|
141
|
+
}
|
|
142
|
+
function unauthorized(deps, mcpPath, error = "unauthorized") {
|
|
143
|
+
return new Response(JSON.stringify({ error }), {
|
|
144
|
+
status: 401,
|
|
145
|
+
headers: {
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
"WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl(deps, mcpPath)}"`
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async function handleMcp(deps, c) {
|
|
152
|
+
const factoryProjectId = c.req.param("projectId") ?? "";
|
|
153
|
+
const mcpPath = new URL(c.req.url).pathname;
|
|
154
|
+
await deps.auth.ensureUser(c);
|
|
155
|
+
const tenant = deps.auth.tenant(c);
|
|
156
|
+
if (!tenant) return unauthorized(deps, mcpPath);
|
|
157
|
+
if (!tenant.orgId) return c.json({ error: "organization_required" }, 403);
|
|
158
|
+
const project = await deps.projects.get({
|
|
159
|
+
orgId: tenant.orgId,
|
|
160
|
+
id: factoryProjectId
|
|
161
|
+
});
|
|
162
|
+
if (!project) return c.json({ error: "not_found" }, 404);
|
|
163
|
+
const { sessionId, persisted } = await ensureConnectorSession(deps, {
|
|
164
|
+
orgId: tenant.orgId,
|
|
165
|
+
userId: tenant.userId,
|
|
166
|
+
factoryProjectId
|
|
167
|
+
});
|
|
168
|
+
const actorProfile = auditActorProfile(getFactoryAuthUser(c));
|
|
169
|
+
const scope = {
|
|
170
|
+
orgId: tenant.orgId,
|
|
171
|
+
factoryProjectId,
|
|
172
|
+
userId: tenant.userId,
|
|
173
|
+
sessionId,
|
|
174
|
+
resourceId: sessionId,
|
|
175
|
+
threadId: sessionId,
|
|
176
|
+
kind: "user",
|
|
177
|
+
interactive: true,
|
|
178
|
+
...actorProfile ? { actorProfile } : {}
|
|
179
|
+
};
|
|
180
|
+
const server = new MCPServer({
|
|
181
|
+
id: `factory-${factoryProjectId}`,
|
|
182
|
+
name: `${project.name ?? "Mastra"} Factory`,
|
|
183
|
+
version: deps.version ?? "1.0.0",
|
|
184
|
+
description: "Manage work items, sessions and decisions in a Mastra Factory.",
|
|
185
|
+
tools: createFactoryMcpTools(scope, deps, { persistedSession: persisted })
|
|
186
|
+
});
|
|
187
|
+
const { req, res } = toReqRes(c.req.raw);
|
|
188
|
+
const done = server.startHTTP({
|
|
189
|
+
url: new URL(c.req.url),
|
|
190
|
+
httpPath: mcpPath,
|
|
191
|
+
req,
|
|
192
|
+
res
|
|
193
|
+
}).catch(() => {
|
|
194
|
+
if (!res.headersSent) {
|
|
195
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
196
|
+
res.end(JSON.stringify({
|
|
197
|
+
jsonrpc: "2.0",
|
|
198
|
+
error: {
|
|
199
|
+
code: -32603,
|
|
200
|
+
message: "Internal server error"
|
|
201
|
+
},
|
|
202
|
+
id: null
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
}).finally(() => void server.close());
|
|
206
|
+
const response = propagateClientDisconnect(await toFetchResponse(res), res);
|
|
207
|
+
if (res.writableEnded) await done;
|
|
208
|
+
return response;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* The Factory's MCP endpoint: Streamable HTTP at
|
|
212
|
+
* `/web/factory/projects/:projectId/mcp`, plus its RFC 9728 protected-resource
|
|
213
|
+
* metadata. Callers sign in with OAuth through the Factory's auth provider and
|
|
214
|
+
* get the tools a web UI session would, under the same permissions.
|
|
215
|
+
*/
|
|
216
|
+
function buildFactoryMcpRoutes(deps) {
|
|
217
|
+
const handler = (c) => handleMcp(deps, c);
|
|
218
|
+
return [...[
|
|
219
|
+
"POST",
|
|
220
|
+
"GET",
|
|
221
|
+
"DELETE"
|
|
222
|
+
].map((method) => registerApiRoute(FACTORY_MCP_PATH, {
|
|
223
|
+
method,
|
|
224
|
+
requiresAuth: false,
|
|
225
|
+
handler
|
|
226
|
+
})), registerApiRoute(`${PROTECTED_RESOURCE_PREFIX}${FACTORY_MCP_PATH}`, {
|
|
227
|
+
method: "GET",
|
|
228
|
+
requiresAuth: false,
|
|
229
|
+
handler: (c) => {
|
|
230
|
+
const mcpPath = new URL(c.req.url).pathname.slice(37);
|
|
231
|
+
return c.json({
|
|
232
|
+
resource: `${deps.publicOrigin}${mcpPath}`,
|
|
233
|
+
authorization_servers: deps.authorizationServers,
|
|
234
|
+
bearer_methods_supported: ["header"],
|
|
235
|
+
resource_name: "Mastra Factory"
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
})];
|
|
239
|
+
}
|
|
240
|
+
//#endregion
|
|
241
|
+
export { FACTORY_MCP_PATH, PLATFORM_MCP_AUTHORIZATION_SERVER, buildFactoryMcpRoutes, connectorSessionId, createFactoryMcpTools, resolveMcpAuthorizationServers };
|
|
242
|
+
|
|
243
|
+
//# sourceMappingURL=routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.js","names":[],"sources":["../../src/mcp/routes.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport { createTool } from '@mastra/core/tools';\nimport type { Tool } from '@mastra/core/tools';\nimport { MCPServer } from '@mastra/mcp';\nimport { toFetchResponse, toReqRes } from 'fetch-to-node';\nimport type { Context } from 'hono';\nimport { z } from 'zod';\n\nimport { getFactoryAuthUser } from '../auth.js';\nimport { PLATFORM_AUTH_PROVIDER } from '../platform-auth-host.js';\nimport type { RouteAuth } from '../routes/route.js';\nimport { ensureFactorySourceSession, FactorySourceSessionResolutionError } from '../session/factory-session.js';\nimport { auditActorProfile } from '../storage/domains/audit/base.js';\nimport type { FactoryProjectsStorage } from '../storage/domains/projects/base.js';\nimport type { SourceControlStorageHandle } from '../storage/domains/source-control/base.js';\nimport { FACTORY_TOOL_ANNOTATIONS } from '../tools/annotations.js';\nimport { createFactoryTools } from '../tools/index.js';\nimport type { FactoryToolDependencies, FactoryToolScope } from '../tools/index.js';\nimport { propagateClientDisconnect } from './disconnect.js';\n\nexport const FACTORY_MCP_PATH = '/web/factory/projects/:projectId/mcp';\nconst PROTECTED_RESOURCE_PREFIX = '/.well-known/oauth-protected-resource';\n\n/**\n * Tools an MCP client never gets. `factory_message_origin` answers the session\n * that started this one; a connector session is started by a person, not a session.\n */\nconst EXCLUDED_TOOLS = new Set(['factory_message_origin']);\n\nexport interface FactoryMcpConfig {\n /** Serve the MCP endpoint. Default `true`; it is never served without an auth provider. */\n enabled?: boolean;\n /**\n * OAuth authorization servers advertised in the protected-resource metadata\n * (RFC 9728) so clients like ChatGPT know where to sign in. Defaults to the\n * `MASTRA_FACTORY_MCP_AUTHORIZATION_SERVER` env var, then to Mastra Platform's\n * AuthKit when the Factory uses Platform auth. The endpoint isn't served\n * without one, since clients would have nowhere to sign in.\n */\n authorizationServers?: string[];\n}\n\n/** Mastra Platform's WorkOS AuthKit, which issues tokens Platform's `/auth/verify` accepts. */\nexport const PLATFORM_MCP_AUTHORIZATION_SERVER = 'https://auth-kit.mastra.ai';\n\nexport function resolveMcpAuthorizationServers(\n providerName: string | undefined,\n config: FactoryMcpConfig | undefined,\n env: Record<string, string | undefined> = process.env,\n): string[] {\n if (config?.authorizationServers?.length) return config.authorizationServers;\n const fromEnv = env.MASTRA_FACTORY_MCP_AUTHORIZATION_SERVER?.trim();\n if (fromEnv) return [fromEnv];\n if (providerName === PLATFORM_AUTH_PROVIDER) return [PLATFORM_MCP_AUTHORIZATION_SERVER];\n return [];\n}\n\nexport interface FactoryMcpRoutesDeps {\n auth: RouteAuth;\n publicOrigin: string;\n authorizationServers: string[];\n projects: Pick<FactoryProjectsStorage, 'get'>;\n /** Source-control handles a connector session can be filed under, tried in order. */\n sourceControls: readonly SourceControlStorageHandle[];\n tools: FactoryToolDependencies;\n version?: string;\n}\n\n/** The connector session a user's MCP client acts from: one per user and Factory, stable across requests. */\nexport function connectorSessionId(orgId: string, userId: string, factoryProjectId: string): string {\n const hex = createHash('sha256').update(`mcp\\0${orgId}\\0${userId}\\0${factoryProjectId}`).digest('hex');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`;\n}\n\n/**\n * Get or create the private user session behind a person's MCP client, so\n * session-scoped tools (start/signal sessions, follow work items) have a real\n * session to act from. A Factory without a connected repository has nowhere to\n * file one; its tools still run against the stable id.\n */\nasync function ensureConnectorSession(\n deps: FactoryMcpRoutesDeps,\n args: { orgId: string; userId: string; factoryProjectId: string },\n): Promise<{ sessionId: string; persisted: boolean }> {\n const sessionId = connectorSessionId(args.orgId, args.userId, args.factoryProjectId);\n if (await deps.tools.sessions.getBySessionId(sessionId)) return { sessionId, persisted: true };\n for (const sourceControl of deps.sourceControls) {\n try {\n await ensureFactorySourceSession({\n sourceControl,\n orgId: args.orgId,\n factoryProjectId: args.factoryProjectId,\n branch: `factory/mcp-${sessionId.slice(0, 8)}`,\n attributeToUserId: args.userId,\n sessionId,\n visibility: 'private',\n });\n return { sessionId, persisted: true };\n } catch (error) {\n if (!(error instanceof FactorySourceSessionResolutionError)) throw error;\n }\n }\n return { sessionId, persisted: false };\n}\n\nfunction workItemUrl(publicOrigin: string, factoryProjectId: string, workItemId: string): string {\n return `${publicOrigin}/factory/${encodeURIComponent(factoryProjectId)}/work-items/${encodeURIComponent(workItemId)}`;\n}\n\n/**\n * `search` and `fetch`, the two tools ChatGPT's connectors and deep research\n * look for, answered from the Factory's work items.\n */\nfunction createSearchFetchTools(\n scope: FactoryToolScope,\n deps: FactoryMcpRoutesDeps,\n inspect: Tool<any, any, any, any> | undefined,\n): Record<string, Tool<any, any, any, any>> {\n const project = { orgId: scope.orgId, factoryProjectId: scope.factoryProjectId };\n return {\n search: createTool({\n id: 'search',\n description:\n 'Search this Factory’s work items by title or id. Returns matching items with ids to pass to `fetch`.',\n inputSchema: z.object({ query: z.string().describe('Words to look for in work item titles.') }),\n execute: async ({ query }) => {\n const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n const items = await deps.tools.workItems.list(project);\n const results = items\n .filter(item => {\n const haystack = `${item.id} ${item.title}`.toLowerCase();\n return terms.every(term => haystack.includes(term));\n })\n .slice(0, 20)\n .map(item => ({\n id: item.id,\n title: item.title,\n url: workItemUrl(deps.publicOrigin, scope.factoryProjectId, item.id),\n }));\n return { results };\n },\n }),\n fetch: createTool({\n id: 'fetch',\n description: 'Fetch everything the Factory knows about one work item, by the id `search` returned.',\n inputSchema: z.object({ id: z.string().describe('Work item id.') }),\n execute: async ({ id }, context) => {\n if (!inspect?.execute) throw new Error('Work item details are unavailable.');\n const details = (await inspect.execute({ workItem: id }, context)) as { item?: { title?: string } };\n return {\n id,\n title: details.item?.title ?? id,\n text: JSON.stringify(details, null, 2),\n url: workItemUrl(deps.publicOrigin, scope.factoryProjectId, id),\n metadata: { factoryProjectId: scope.factoryProjectId },\n };\n },\n }),\n };\n}\n\n/** The tools a signed-in person's MCP client sees for one Factory. */\nexport function createFactoryMcpTools(\n scope: FactoryToolScope,\n deps: FactoryMcpRoutesDeps,\n options: { persistedSession?: boolean } = {},\n): Record<string, Tool<any, any, any, any>> {\n const factoryTools = createFactoryTools({ scope, deps: deps.tools }) as Record<string, Tool<any, any, any, any>>;\n const tools: Record<string, Tool<any, any, any, any>> = {};\n for (const [name, tool] of Object.entries(factoryTools)) {\n if (EXCLUDED_TOOLS.has(name)) continue;\n // Starting a sub-session needs a real parent session row to file it under.\n if (name === 'factory_start_session' && options.persistedSession === false) continue;\n tools[name] = tool;\n }\n Object.assign(tools, createSearchFetchTools(scope, deps, factoryTools.factory_inspect_work_item));\n for (const [name, tool] of Object.entries(tools)) {\n const annotations = FACTORY_TOOL_ANNOTATIONS[name];\n if (annotations) tool.mcp = { ...tool.mcp, annotations };\n }\n return tools;\n}\n\nfunction resourceMetadataUrl(deps: FactoryMcpRoutesDeps, mcpPath: string): string {\n return `${deps.publicOrigin}${PROTECTED_RESOURCE_PREFIX}${mcpPath}`;\n}\n\nfunction unauthorized(deps: FactoryMcpRoutesDeps, mcpPath: string, error = 'unauthorized'): Response {\n return new Response(JSON.stringify({ error }), {\n status: 401,\n headers: {\n 'Content-Type': 'application/json',\n 'WWW-Authenticate': `Bearer resource_metadata=\"${resourceMetadataUrl(deps, mcpPath)}\"`,\n },\n });\n}\n\nasync function handleMcp(deps: FactoryMcpRoutesDeps, c: Context): Promise<Response> {\n const factoryProjectId = c.req.param('projectId') ?? '';\n const mcpPath = new URL(c.req.url).pathname;\n\n await deps.auth.ensureUser(c);\n const tenant = deps.auth.tenant(c);\n if (!tenant) return unauthorized(deps, mcpPath);\n if (!tenant.orgId) return c.json({ error: 'organization_required' }, 403);\n\n const project = await deps.projects.get({ orgId: tenant.orgId, id: factoryProjectId });\n if (!project) return c.json({ error: 'not_found' }, 404);\n\n const { sessionId, persisted } = await ensureConnectorSession(deps, {\n orgId: tenant.orgId,\n userId: tenant.userId,\n factoryProjectId,\n });\n const actorProfile = auditActorProfile(getFactoryAuthUser(c));\n const scope: FactoryToolScope = {\n orgId: tenant.orgId,\n factoryProjectId,\n userId: tenant.userId,\n sessionId,\n resourceId: sessionId,\n threadId: sessionId,\n kind: 'user',\n // A signed-in person is driving the client: the same reach as a web UI turn.\n interactive: true,\n ...(actorProfile ? { actorProfile } : {}),\n };\n\n const server = new MCPServer({\n id: `factory-${factoryProjectId}`,\n name: `${project.name ?? 'Mastra'} Factory`,\n version: deps.version ?? '1.0.0',\n description: 'Manage work items, sessions and decisions in a Mastra Factory.',\n tools: createFactoryMcpTools(scope, deps, { persistedSession: persisted }),\n });\n\n const { req, res } = toReqRes(c.req.raw);\n const done = server\n .startHTTP({ url: new URL(c.req.url), httpPath: mcpPath, req, res })\n .catch(() => {\n if (!res.headersSent) {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal server error' }, id: null }),\n );\n }\n })\n .finally(() => void server.close());\n const response = propagateClientDisconnect(await toFetchResponse(res), res);\n if (res.writableEnded) await done;\n return response;\n}\n\n/**\n * The Factory's MCP endpoint: Streamable HTTP at\n * `/web/factory/projects/:projectId/mcp`, plus its RFC 9728 protected-resource\n * metadata. Callers sign in with OAuth through the Factory's auth provider and\n * get the tools a web UI session would, under the same permissions.\n */\nexport function buildFactoryMcpRoutes(deps: FactoryMcpRoutesDeps): ApiRoute[] {\n const handler = (c: unknown) => handleMcp(deps, c as Context);\n return [\n ...(['POST', 'GET', 'DELETE'] as const).map(method =>\n registerApiRoute(FACTORY_MCP_PATH, { method, requiresAuth: false, handler }),\n ),\n registerApiRoute(`${PROTECTED_RESOURCE_PREFIX}${FACTORY_MCP_PATH}`, {\n method: 'GET',\n requiresAuth: false,\n handler: c => {\n const mcpPath = new URL(c.req.url).pathname.slice(PROTECTED_RESOURCE_PREFIX.length);\n return c.json({\n resource: `${deps.publicOrigin}${mcpPath}`,\n authorization_servers: deps.authorizationServers,\n bearer_methods_supported: ['header'],\n resource_name: 'Mastra Factory',\n });\n },\n }),\n ];\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-display-name.d.ts","sourceRoot":"","sources":["../src/provider-display-name.ts"],"names":[],"mappings":"AASA,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQ5D"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/provider-display-name.ts
|
|
2
|
+
const PROVIDER_DISPLAY_NAMES = {
|
|
3
|
+
anthropic: "Anthropic",
|
|
4
|
+
"github-copilot": "GitHub Copilot",
|
|
5
|
+
"kimi-code-plan-cn": "Kimi",
|
|
6
|
+
openai: "OpenAI",
|
|
7
|
+
"openai-codex": "OpenAI",
|
|
8
|
+
xai: "xAI"
|
|
9
|
+
};
|
|
10
|
+
function providerDisplayName(provider) {
|
|
11
|
+
return PROVIDER_DISPLAY_NAMES[provider] ?? provider.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
export { providerDisplayName };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=provider-display-name.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-display-name.js","names":[],"sources":["../src/provider-display-name.ts"],"sourcesContent":["const PROVIDER_DISPLAY_NAMES: Record<string, string> = {\n anthropic: 'Anthropic',\n 'github-copilot': 'GitHub Copilot',\n 'kimi-code-plan-cn': 'Kimi',\n openai: 'OpenAI',\n 'openai-codex': 'OpenAI',\n xai: 'xAI',\n};\n\nexport function providerDisplayName(provider: string): string {\n return (\n PROVIDER_DISPLAY_NAMES[provider] ??\n provider\n .split('-')\n .map(part => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ')\n );\n}\n"],"mappings":";AAAA,MAAM,yBAAiD;CACrD,WAAW;CACX,kBAAkB;CAClB,qBAAqB;CACrB,QAAQ;CACR,gBAAgB;CAChB,KAAK;AACP;AAEA,SAAgB,oBAAoB,UAA0B;CAC5D,OACE,uBAAuB,aACvB,SACG,MAAM,GAAG,CAAC,CACV,KAAI,SAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,CACzD,KAAK,GAAG;AAEf"}
|
package/dist/routes/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { WEB_OAUTH_PROVIDERS, getAuthProviderId, listTenantCredentialsForRequest, resolveCredentialContext, tenantOrgId } from "./provider-credentials.js";
|
|
2
2
|
import { resolveFactoryModelPolicy } from "../session/credential-policy.js";
|
|
3
3
|
import "../session/memory-settings-hydration.js";
|
|
4
4
|
import { factoryMemorySettingsUserId } from "../storage/domains/memory-settings/base.js";
|
|
@@ -57,7 +57,7 @@ async function listProviders({ controller, authStorage, tenantCredentials, deplo
|
|
|
57
57
|
else if (authStorage?.hasStoredApiKey(model.provider)) source = "stored";
|
|
58
58
|
else if (model.apiKeyEnvVar && process.env[model.apiKeyEnvVar]) source = "env";
|
|
59
59
|
else if (model.hasApiKey) source = "env";
|
|
60
|
-
const flowKind =
|
|
60
|
+
const flowKind = WEB_OAUTH_PROVIDERS[model.provider]?.flow;
|
|
61
61
|
seen.set(model.provider, {
|
|
62
62
|
provider: model.provider,
|
|
63
63
|
envVar: model.apiKeyEnvVar,
|