@agent-deck/backend 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/config/oauth-redirect.d.ts +18 -0
  3. package/dist/config/oauth-redirect.d.ts.map +1 -0
  4. package/dist/config/oauth-redirect.js +57 -0
  5. package/dist/config/oauth-redirect.js.map +1 -0
  6. package/dist/config/shared-oauth-apps.d.ts +18 -0
  7. package/dist/config/shared-oauth-apps.d.ts.map +1 -0
  8. package/dist/config/shared-oauth-apps.js +35 -0
  9. package/dist/config/shared-oauth-apps.js.map +1 -0
  10. package/dist/data/default-mcp-services.d.ts +14 -0
  11. package/dist/data/default-mcp-services.d.ts.map +1 -0
  12. package/dist/data/default-mcp-services.js +52 -0
  13. package/dist/data/default-mcp-services.js.map +1 -0
  14. package/dist/data/oauth-provider-guides.d.ts +35 -0
  15. package/dist/data/oauth-provider-guides.d.ts.map +1 -0
  16. package/dist/data/oauth-provider-guides.js +298 -0
  17. package/dist/data/oauth-provider-guides.js.map +1 -0
  18. package/dist/data/seed-default-services.d.ts +3 -0
  19. package/dist/data/seed-default-services.d.ts.map +1 -0
  20. package/dist/data/seed-default-services.js +14 -0
  21. package/dist/data/seed-default-services.js.map +1 -0
  22. package/dist/lib/mcp-connection-error.d.ts +4 -0
  23. package/dist/lib/mcp-connection-error.d.ts.map +1 -0
  24. package/dist/lib/mcp-connection-error.js +45 -0
  25. package/dist/lib/mcp-connection-error.js.map +1 -0
  26. package/dist/lib/mcp-oauth-discovery.d.ts +16 -0
  27. package/dist/lib/mcp-oauth-discovery.d.ts.map +1 -0
  28. package/dist/lib/mcp-oauth-discovery.js +161 -0
  29. package/dist/lib/mcp-oauth-discovery.js.map +1 -0
  30. package/dist/lib/oauth-token-response.d.ts +11 -0
  31. package/dist/lib/oauth-token-response.d.ts.map +1 -0
  32. package/dist/lib/oauth-token-response.js +48 -0
  33. package/dist/lib/oauth-token-response.js.map +1 -0
  34. package/dist/lib/pkce.d.ts +7 -0
  35. package/dist/lib/pkce.d.ts.map +1 -0
  36. package/dist/lib/pkce.js +11 -0
  37. package/dist/lib/pkce.js.map +1 -0
  38. package/dist/mcp-server.d.ts +4 -3
  39. package/dist/mcp-server.d.ts.map +1 -1
  40. package/dist/mcp-server.js +128 -32
  41. package/dist/mcp-server.js.map +1 -1
  42. package/dist/mcp-session-binding.d.ts +29 -0
  43. package/dist/mcp-session-binding.d.ts.map +1 -0
  44. package/dist/mcp-session-binding.js +74 -0
  45. package/dist/mcp-session-binding.js.map +1 -0
  46. package/dist/models/database.d.ts +1 -0
  47. package/dist/models/database.d.ts.map +1 -1
  48. package/dist/models/database.js +4 -0
  49. package/dist/models/database.js.map +1 -1
  50. package/dist/routes/oauth.d.ts.map +1 -1
  51. package/dist/routes/oauth.js +85 -48
  52. package/dist/routes/oauth.js.map +1 -1
  53. package/dist/server/index.d.ts.map +1 -1
  54. package/dist/server/index.js +6 -0
  55. package/dist/server/index.js.map +1 -1
  56. package/dist/services/collection-warning-service.d.ts.map +1 -1
  57. package/dist/services/collection-warning-service.js +1 -5
  58. package/dist/services/collection-warning-service.js.map +1 -1
  59. package/dist/services/config-manager.d.ts.map +1 -1
  60. package/dist/services/config-manager.js +5 -5
  61. package/dist/services/config-manager.js.map +1 -1
  62. package/dist/services/mcp-client-manager.d.ts.map +1 -1
  63. package/dist/services/mcp-client-manager.js +6 -1
  64. package/dist/services/mcp-client-manager.js.map +1 -1
  65. package/dist/services/mcp-discovery-service.d.ts +1 -0
  66. package/dist/services/mcp-discovery-service.d.ts.map +1 -1
  67. package/dist/services/mcp-discovery-service.js +2 -79
  68. package/dist/services/mcp-discovery-service.js.map +1 -1
  69. package/dist/services/oauth-connect-service.d.ts +26 -0
  70. package/dist/services/oauth-connect-service.d.ts.map +1 -0
  71. package/dist/services/oauth-connect-service.js +129 -0
  72. package/dist/services/oauth-connect-service.js.map +1 -0
  73. package/dist/services/oauth-manager.d.ts.map +1 -1
  74. package/dist/services/oauth-manager.js +74 -70
  75. package/dist/services/oauth-manager.js.map +1 -1
  76. package/dist/services/service-manager.d.ts +2 -0
  77. package/dist/services/service-manager.d.ts.map +1 -1
  78. package/dist/services/service-manager.js +22 -3
  79. package/dist/services/service-manager.js.map +1 -1
  80. package/package.json +4 -4
  81. package/static-ui/assets/AgentDeckLogo3-CEDxeHlz.png +0 -0
  82. package/static-ui/assets/index-CPNBMDJn.js +354 -0
  83. package/static-ui/assets/index-Df5pC3f-.css +1 -0
  84. package/static-ui/favicon.png +0 -0
  85. package/static-ui/index.html +2 -2
  86. package/static-ui/assets/AgentDeckLogo2-z3pVqJJ3.png +0 -0
  87. package/static-ui/assets/index-BMpKtwqp.js +0 -339
  88. package/static-ui/assets/index-BnA3AsqY.css +0 -1
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.discoverMcpOAuthRequirements = discoverMcpOAuthRequirements;
4
+ const oauth_provider_guides_1 = require("../data/oauth-provider-guides");
5
+ function finalizeOAuthDiscovery(serviceUrl, oauth, authServers = []) {
6
+ const provider = oauth.provider ?? (0, oauth_provider_guides_1.inferOAuthProvider)(serviceUrl, authServers);
7
+ const setupMode = (0, oauth_provider_guides_1.resolveOAuthSetupMode)(provider, oauth.supportsDynamicRegistration);
8
+ return {
9
+ ...oauth,
10
+ provider,
11
+ setupMode,
12
+ supportsDynamicRegistration: setupMode === 'dynamic' ? true : oauth.supportsDynamicRegistration ?? false,
13
+ };
14
+ }
15
+ function protectedResourceCandidates(serviceUrl) {
16
+ const parsed = new URL(serviceUrl);
17
+ const path = parsed.pathname.replace(/\/$/, '');
18
+ const candidates = new Set([
19
+ `${parsed.origin}/.well-known/oauth-protected-resource`,
20
+ ]);
21
+ if (path && path !== '/') {
22
+ candidates.add(`${parsed.origin}/.well-known/oauth-protected-resource${path}`);
23
+ candidates.add(`${parsed.origin}/.well-known/oauth-protected-resource${path}/`);
24
+ }
25
+ return [...candidates];
26
+ }
27
+ async function fetchJson(url) {
28
+ try {
29
+ const response = await fetch(url, {
30
+ headers: { Accept: 'application/json' },
31
+ });
32
+ if (!response.ok) {
33
+ return null;
34
+ }
35
+ return response.json();
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ async function fetchAuthorizationServerMetadata(issuerOrAsUrl) {
42
+ const trimmed = issuerOrAsUrl.replace(/\/$/, '');
43
+ const candidates = trimmed.includes('/.well-known/')
44
+ ? [trimmed]
45
+ : [
46
+ `${trimmed}/.well-known/oauth-authorization-server`,
47
+ `${trimmed}/.well-known/openid-configuration`,
48
+ ];
49
+ for (const candidate of candidates) {
50
+ const data = await fetchJson(candidate);
51
+ if (data && typeof data === 'object') {
52
+ return data;
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ function applyKnownProviderFallbacks(serviceUrl, authServers, oauth) {
58
+ if (authServers.some((server) => server.includes('github.com/login/oauth'))) {
59
+ return finalizeOAuthDiscovery(serviceUrl, {
60
+ ...oauth,
61
+ required: true,
62
+ provider: 'github',
63
+ authorizationUrl: 'https://github.com/login/oauth/authorize',
64
+ tokenUrl: 'https://github.com/login/oauth/access_token',
65
+ supportsDynamicRegistration: false,
66
+ }, authServers);
67
+ }
68
+ return finalizeOAuthDiscovery(serviceUrl, oauth, authServers);
69
+ }
70
+ async function resolveFromProtectedResource(serviceUrl, metadata) {
71
+ const oauth = {
72
+ required: true,
73
+ resourceName: metadata.resource_name ?? metadata.resource,
74
+ scopesSupported: metadata.scopes_supported ?? [],
75
+ bearerMethodsSupported: metadata.bearer_methods_supported ?? [],
76
+ };
77
+ const authServers = metadata.authorization_servers ?? [];
78
+ if (authServers.length === 0) {
79
+ return finalizeOAuthDiscovery(serviceUrl, oauth, authServers);
80
+ }
81
+ const asMeta = await fetchAuthorizationServerMetadata(authServers[0]);
82
+ if (asMeta) {
83
+ oauth.issuer = asMeta.issuer;
84
+ oauth.authorizationUrl = asMeta.authorization_endpoint;
85
+ oauth.tokenUrl = asMeta.token_endpoint;
86
+ oauth.scopesSupported = asMeta.scopes_supported ?? oauth.scopesSupported;
87
+ oauth.supportsDynamicRegistration = Boolean(asMeta.registration_endpoint);
88
+ }
89
+ return applyKnownProviderFallbacks(serviceUrl, authServers, oauth);
90
+ }
91
+ async function probeWwwAuthenticate(serviceUrl) {
92
+ const headers = {
93
+ Accept: 'application/json, text/event-stream',
94
+ 'Content-Type': 'application/json',
95
+ };
96
+ for (const method of ['GET', 'POST']) {
97
+ try {
98
+ const init = method === 'GET'
99
+ ? { method, headers: { Accept: headers.Accept } }
100
+ : {
101
+ method,
102
+ headers,
103
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: {} }),
104
+ };
105
+ const response = await fetch(serviceUrl, init);
106
+ const wwwAuth = response.headers.get('www-authenticate');
107
+ if (wwwAuth) {
108
+ return wwwAuth;
109
+ }
110
+ }
111
+ catch {
112
+ // try next method
113
+ }
114
+ }
115
+ return null;
116
+ }
117
+ function parseResourceMetadataUrl(wwwAuthenticate) {
118
+ const match = wwwAuthenticate.match(/resource_metadata="([^"]+)"/i);
119
+ return match?.[1] ?? null;
120
+ }
121
+ async function discoverMcpOAuthRequirements(serviceUrl) {
122
+ const oauth = { required: false };
123
+ try {
124
+ for (const candidate of protectedResourceCandidates(serviceUrl)) {
125
+ const data = await fetchJson(candidate);
126
+ if (data && typeof data === 'object') {
127
+ return resolveFromProtectedResource(serviceUrl, data);
128
+ }
129
+ }
130
+ const parsed = new URL(serviceUrl);
131
+ const asData = await fetchAuthorizationServerMetadata(parsed.origin);
132
+ if (asData?.authorization_endpoint) {
133
+ return finalizeOAuthDiscovery(serviceUrl, {
134
+ required: true,
135
+ issuer: asData.issuer,
136
+ authorizationUrl: asData.authorization_endpoint,
137
+ tokenUrl: asData.token_endpoint,
138
+ scopesSupported: asData.scopes_supported ?? [],
139
+ supportsDynamicRegistration: Boolean(asData.registration_endpoint),
140
+ });
141
+ }
142
+ const wwwAuth = await probeWwwAuthenticate(serviceUrl);
143
+ if (wwwAuth) {
144
+ const resourceMetadataUrl = parseResourceMetadataUrl(wwwAuth);
145
+ if (resourceMetadataUrl) {
146
+ const data = await fetchJson(resourceMetadataUrl);
147
+ if (data && typeof data === 'object') {
148
+ return resolveFromProtectedResource(serviceUrl, data);
149
+ }
150
+ }
151
+ if (wwwAuth.toLowerCase().includes('oauth') || wwwAuth.toLowerCase().includes('bearer')) {
152
+ return finalizeOAuthDiscovery(serviceUrl, { required: true });
153
+ }
154
+ }
155
+ }
156
+ catch (error) {
157
+ oauth.error = error instanceof Error ? error.message : 'OAuth detection failed';
158
+ }
159
+ return oauth;
160
+ }
161
+ //# sourceMappingURL=mcp-oauth-discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-oauth-discovery.js","sourceRoot":"","sources":["../../src/lib/mcp-oauth-discovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ export type OAuthTokenResponseBody = {
2
+ access_token: string;
3
+ refresh_token?: string;
4
+ expires_in?: number;
5
+ token_type?: string;
6
+ scope?: string;
7
+ };
8
+ /** Parse OAuth token endpoint body (JSON or application/x-www-form-urlencoded). */
9
+ export declare function parseOAuthTokenResponse(response: Response): Promise<OAuthTokenResponseBody>;
10
+ export declare function oauthTokenRequestHeaders(): Record<string, string>;
11
+ //# sourceMappingURL=oauth-token-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-token-response.d.ts","sourceRoot":"","sources":["../../src/lib/oauth-token-response.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,mFAAmF;AACnF,wBAAsB,uBAAuB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,sBAAsB,CAAC,CA0CjG;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAKjE"}
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseOAuthTokenResponse = parseOAuthTokenResponse;
4
+ exports.oauthTokenRequestHeaders = oauthTokenRequestHeaders;
5
+ /** Parse OAuth token endpoint body (JSON or application/x-www-form-urlencoded). */
6
+ async function parseOAuthTokenResponse(response) {
7
+ const contentType = response.headers.get('content-type') ?? '';
8
+ const text = await response.text();
9
+ const trimmed = text.trim();
10
+ if (!trimmed) {
11
+ throw new Error('OAuth token response was empty');
12
+ }
13
+ if (contentType.includes('application/json') || trimmed.startsWith('{')) {
14
+ const data = JSON.parse(trimmed);
15
+ if (data.error) {
16
+ throw new Error(`OAuth token error: ${data.error}${data.error_description ? ` — ${data.error_description}` : ''}`);
17
+ }
18
+ if (!data.access_token) {
19
+ throw new Error('OAuth token JSON response missing access_token');
20
+ }
21
+ return data;
22
+ }
23
+ const params = new URLSearchParams(trimmed);
24
+ const oauthError = params.get('error');
25
+ if (oauthError) {
26
+ const description = params.get('error_description');
27
+ throw new Error(`OAuth token error: ${oauthError}${description ? ` — ${description}` : ''}`);
28
+ }
29
+ const accessToken = params.get('access_token');
30
+ if (!accessToken) {
31
+ throw new Error(`OAuth token response missing access_token: ${trimmed.slice(0, 200)}`);
32
+ }
33
+ const expiresIn = params.get('expires_in');
34
+ return {
35
+ access_token: accessToken,
36
+ refresh_token: params.get('refresh_token') ?? undefined,
37
+ expires_in: expiresIn ? Number(expiresIn) : undefined,
38
+ token_type: params.get('token_type') ?? undefined,
39
+ scope: params.get('scope') ?? undefined,
40
+ };
41
+ }
42
+ function oauthTokenRequestHeaders() {
43
+ return {
44
+ 'Content-Type': 'application/x-www-form-urlencoded',
45
+ Accept: 'application/json',
46
+ };
47
+ }
48
+ //# sourceMappingURL=oauth-token-response.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-token-response.js","sourceRoot":"","sources":["../../src/lib/oauth-token-response.ts"],"names":[],"mappings":";;AASA,0DA0CC;AAED,4DAKC;AAlDD,mFAAmF;AAC5E,KAAK,UAAU,uBAAuB,CAAC,QAAkB;IAC9D,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;IAC/D,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACnC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAE5B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACpD,CAAC;IAED,IAAI,WAAW,CAAC,QAAQ,CAAC,kBAAkB,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAA4E,CAAC;QAC5G,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CACb,sBAAsB,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,iBAAiB,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAClG,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;QACpE,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACvC,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,sBAAsB,UAAU,GAAG,WAAW,CAAC,CAAC,CAAC,MAAM,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC/F,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IAC/C,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,8CAA8C,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IACzF,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC3C,OAAO;QACL,YAAY,EAAE,WAAW;QACzB,aAAa,EAAE,MAAM,CAAC,GAAG,CAAC,eAAe,CAAC,IAAI,SAAS;QACvD,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS;QACrD,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,SAAS;QACjD,KAAK,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,SAAS;KACxC,CAAC;AACJ,CAAC;AAED,SAAgB,wBAAwB;IACtC,OAAO;QACL,cAAc,EAAE,mCAAmC;QACnD,MAAM,EAAE,kBAAkB;KAC3B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,7 @@
1
+ export interface PkcePair {
2
+ codeVerifier: string;
3
+ codeChallenge: string;
4
+ }
5
+ /** RFC 7636 PKCE with S256 code challenge. */
6
+ export declare function generatePkcePair(): PkcePair;
7
+ //# sourceMappingURL=pkce.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.d.ts","sourceRoot":"","sources":["../../src/lib/pkce.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,QAAQ;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,8CAA8C;AAC9C,wBAAgB,gBAAgB,IAAI,QAAQ,CAI3C"}
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generatePkcePair = generatePkcePair;
4
+ const crypto_1 = require("crypto");
5
+ /** RFC 7636 PKCE with S256 code challenge. */
6
+ function generatePkcePair() {
7
+ const codeVerifier = (0, crypto_1.randomBytes)(32).toString('base64url');
8
+ const codeChallenge = (0, crypto_1.createHash)('sha256').update(codeVerifier).digest('base64url');
9
+ return { codeVerifier, codeChallenge };
10
+ }
11
+ //# sourceMappingURL=pkce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../src/lib/pkce.ts"],"names":[],"mappings":";;AAQA,4CAIC;AAZD,mCAAiD;AAOjD,8CAA8C;AAC9C,SAAgB,gBAAgB;IAC9B,MAAM,YAAY,GAAG,IAAA,oBAAW,EAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IAC3D,MAAM,aAAa,GAAG,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACpF,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,CAAC;AACzC,CAAC"}
@@ -6,15 +6,16 @@ export declare class AgentDeckMCPServer {
6
6
  private sessions;
7
7
  /** Set only while registerTools/registerResources run for a new session server. */
8
8
  private mcpServerForRegistration;
9
- /** Workspace root per MCP session → resolves `.agent-deck/deck.yaml`. */
10
- private workspaceBySession;
9
+ /** Per-session workspace + optional deck override (see mcp-session-binding.ts). */
10
+ private sessionBinding;
11
11
  private activeSessionId;
12
12
  private get server();
13
13
  constructor(port?: number, backendUrl?: string);
14
14
  private createMcpServer;
15
15
  private getSessionId;
16
- private getBoundWorkspace;
17
16
  private getAgentHeaders;
17
+ private fetchDeck;
18
+ private buildBindingPayload;
18
19
  private callBackendAPI;
19
20
  private getBoundDeckId;
20
21
  private toolResult;
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAQA,OAAO,OAA8B,MAAM,SAAS,CAAC;AAuBrD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,GAAG,CAAsB;IACjC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAiC;IACjD,mFAAmF;IACnF,OAAO,CAAC,wBAAwB,CAAwB;IACxD,yEAAyE;IACzE,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,eAAe,CAAqB;IAE5C,OAAO,KAAK,MAAM,GAKjB;gBAEW,IAAI,GAAE,MAAa,EAAE,UAAU,GAAE,MAAgC;IAc7E,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,iBAAiB;IAKzB,OAAO,CAAC,eAAe;YAQT,cAAc;YAwCd,cAAc;IAQ5B,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,UAAU;IA8wBlB,OAAO,CAAC,cAAc;IA2LtB,OAAO,CAAC,WAAW;IA2CnB,OAAO,CAAC,kBAAkB;YAKZ,aAAa;YAuDb,uBAAuB;IAiB/B,KAAK;IAiCL,IAAI;CAeX"}
1
+ {"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":"AAQA,OAAO,OAA8B,MAAM,SAAS,CAAC;AA0BrD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,GAAG,CAAsB;IACjC,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,QAAQ,CAAiC;IACjD,mFAAmF;IACnF,OAAO,CAAC,wBAAwB,CAAwB;IACxD,mFAAmF;IACnF,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,eAAe,CAAqB;IAE5C,OAAO,KAAK,MAAM,GAKjB;gBAEW,IAAI,GAAE,MAAa,EAAE,UAAU,GAAE,MAAgC;IAc7E,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,eAAe;YAIT,SAAS;YAkBT,mBAAmB;YAYnB,cAAc;YAwCd,cAAc;IAQ5B,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,UAAU;IAm2BlB,OAAO,CAAC,cAAc;IA2LtB,OAAO,CAAC,WAAW;IA2CnB,OAAO,CAAC,kBAAkB;YAKZ,aAAa;YAuDb,uBAAuB;IAiB/B,KAAK;IAmCL,IAAI;CAeX"}
@@ -14,8 +14,8 @@ const promises_1 = __importDefault(require("node:fs/promises"));
14
14
  const node_path_1 = __importDefault(require("node:path"));
15
15
  const zod_1 = require("zod");
16
16
  const repo_deck_1 = require("./scope/repo-deck");
17
- const shared_2 = require("@agent-deck/shared");
18
17
  const version_1 = require("./lib/version");
18
+ const mcp_session_binding_1 = require("./mcp-session-binding");
19
19
  const agentClientHeaders = {
20
20
  [shared_1.AGENT_DECK_CLIENT_HEADER]: shared_1.AGENT_DECK_AGENT_CLIENT,
21
21
  Accept: 'application/json',
@@ -27,8 +27,8 @@ class AgentDeckMCPServer {
27
27
  sessions = new Map();
28
28
  /** Set only while registerTools/registerResources run for a new session server. */
29
29
  mcpServerForRegistration;
30
- /** Workspace root per MCP session → resolves `.agent-deck/deck.yaml`. */
31
- workspaceBySession = new Map();
30
+ /** Per-session workspace + optional deck override (see mcp-session-binding.ts). */
31
+ sessionBinding;
32
32
  activeSessionId;
33
33
  get server() {
34
34
  if (!this.mcpServerForRegistration) {
@@ -42,10 +42,10 @@ class AgentDeckMCPServer {
42
42
  this.app = (0, express_1.default)();
43
43
  this.app.use(express_1.default.json());
44
44
  this.setupRoutes();
45
- const envWorkspace = process.env.AGENT_DECK_WORKSPACE?.trim();
46
- if (envWorkspace) {
47
- this.workspaceBySession.set('default', envWorkspace);
48
- }
45
+ this.sessionBinding = new mcp_session_binding_1.McpSessionBindingStore({
46
+ workspace: process.env.AGENT_DECK_WORKSPACE,
47
+ deckId: process.env.AGENT_DECK_DECK_ID,
48
+ });
49
49
  }
50
50
  createMcpServer() {
51
51
  this.mcpServerForRegistration = new mcp_js_1.McpServer({
@@ -61,15 +61,31 @@ class AgentDeckMCPServer {
61
61
  getSessionId() {
62
62
  return this.activeSessionId ?? 'default';
63
63
  }
64
- getBoundWorkspace() {
65
- const sessionId = this.getSessionId();
66
- return this.workspaceBySession.get(sessionId) ?? process.env.AGENT_DECK_WORKSPACE?.trim();
67
- }
68
64
  getAgentHeaders() {
69
- const workspace = this.getBoundWorkspace();
65
+ return this.sessionBinding.getAgentHeaders(this.getSessionId());
66
+ }
67
+ async fetchDeck(deckId) {
68
+ const deck = await fetch(`${this.backendUrl}/api/decks/${deckId}`, {
69
+ headers: {
70
+ ...agentClientHeaders,
71
+ Accept: 'application/json',
72
+ },
73
+ });
74
+ const body = (await deck.json());
75
+ if (!deck.ok || !body.success || !body.data?.id) {
76
+ throw new Error(body.error ?? `Deck not found: ${deckId}`);
77
+ }
78
+ return body.data;
79
+ }
80
+ async buildBindingPayload(sessionId, manifestDeckId) {
81
+ const snapshot = this.sessionBinding.getBinding(sessionId);
82
+ const deck = await this.callBackendAPI('/api/scope/deck');
70
83
  return {
71
- ...agentClientHeaders,
72
- ...(workspace ? { [shared_1.AGENT_DECK_WORKSPACE_HEADER]: workspace } : {}),
84
+ workspaceRoot: snapshot.workspaceRoot,
85
+ deck_id: deck?.id,
86
+ deck_name: deck?.name,
87
+ deck_source: (0, mcp_session_binding_1.resolveDeckBindingSource)(snapshot, manifestDeckId),
88
+ session_deck_override: this.sessionBinding.hasSessionDeckOverride(sessionId),
73
89
  };
74
90
  }
75
91
  async callBackendAPI(endpoint, init = {}) {
@@ -116,7 +132,7 @@ class AgentDeckMCPServer {
116
132
  async getBoundDeckId() {
117
133
  const deck = await this.callBackendAPI('/api/scope/deck');
118
134
  if (!deck?.id) {
119
- throw new Error('No bound deck — call bind_workspace or setup_repo_deck first');
135
+ throw new Error('No bound deck — call bind_workspace (optionally with deckId) or switch_bound_deck first');
120
136
  }
121
137
  return deck.id;
122
138
  }
@@ -129,12 +145,31 @@ class AgentDeckMCPServer {
129
145
  setupTools() {
130
146
  this.server.registerTool("bind_workspace", {
131
147
  title: "Bind Workspace",
132
- description: "Bind an MCP session to a repo workspace root. Reads .agent-deck/deck.yaml to resolve the deck for this project.",
133
- inputSchema: { workspaceRoot: zod_1.z.string() },
134
- }, async ({ workspaceRoot }) => {
148
+ description: "Bind this MCP session to a workspace root. Pass deckId for a session-only deck override (same path, different concurrent agents). Without deckId, uses .agent-deck/deck.yaml when present.",
149
+ inputSchema: {
150
+ workspaceRoot: zod_1.z.string(),
151
+ deckId: zod_1.z.string().uuid().optional(),
152
+ },
153
+ }, async ({ workspaceRoot, deckId }) => {
135
154
  try {
136
155
  const sessionId = this.getSessionId();
137
- this.workspaceBySession.set(sessionId, workspaceRoot);
156
+ this.sessionBinding.setWorkspace(sessionId, workspaceRoot);
157
+ if (deckId) {
158
+ const deck = await this.fetchDeck(deckId);
159
+ this.sessionBinding.setDeckId(sessionId, deckId);
160
+ const payload = await this.buildBindingPayload(sessionId);
161
+ return {
162
+ content: [{
163
+ type: "text",
164
+ text: JSON.stringify({
165
+ ...payload,
166
+ deck_name: deck.name,
167
+ message: 'Session bound with deck override. .agent-deck/deck.yaml is unchanged; other MCP sessions can use a different deck at the same path.',
168
+ }, null, 2),
169
+ }],
170
+ };
171
+ }
172
+ this.sessionBinding.clearDeckId(sessionId);
138
173
  const resolved = await fetch(`${this.backendUrl}/api/scope/resolve`, {
139
174
  method: 'POST',
140
175
  headers: { ...agentClientHeaders, 'Content-Type': 'application/json' },
@@ -142,31 +177,90 @@ class AgentDeckMCPServer {
142
177
  });
143
178
  const body = (await resolved.json());
144
179
  if (!body.success || !body.data) {
145
- throw new Error(body.error ?? 'Failed to resolve workspace deck');
180
+ throw new Error(body.error ??
181
+ 'Failed to resolve workspace deck. Pass deckId to bind without deck.yaml, or call setup_repo_deck.');
146
182
  }
147
183
  const { data } = body;
184
+ const payload = await this.buildBindingPayload(sessionId, data.manifest.deck_id);
148
185
  return {
149
186
  content: [{
150
187
  type: "text",
151
188
  text: JSON.stringify({
152
- workspaceRoot,
189
+ ...payload,
153
190
  manifestPath: data.manifestPath,
154
- deck_id: data.manifest.deck_id,
155
- deck_name: data.deck.name,
191
+ message: 'Session bound via .agent-deck/deck.yaml.',
156
192
  }, null, 2),
157
193
  }],
158
194
  };
159
195
  }
160
196
  catch (error) {
161
197
  const message = String(error);
162
- const hint = message.includes('deck.yaml')
163
- ? ' Call setup_repo_deck with workspaceRoot and deckId to create .agent-deck/deck.yaml, or get_repo_deck_status to diagnose.'
198
+ const hint = message.includes('deck.yaml') || message.includes('No .agent-deck')
199
+ ? ' Pass deckId on bind_workspace, call setup_repo_deck, or get_repo_deck_status to diagnose.'
164
200
  : '';
165
201
  return {
166
202
  content: [{ type: "text", text: JSON.stringify({ error: message + hint }, null, 2) }],
167
203
  };
168
204
  }
169
205
  });
206
+ this.server.registerTool("switch_bound_deck", {
207
+ title: "Switch Bound Deck",
208
+ description: "Switch the deck for this MCP session only. Does not modify .agent-deck/deck.yaml. Use when multiple agent sessions share the same workspace path.",
209
+ inputSchema: { deckId: zod_1.z.string().uuid() },
210
+ }, async ({ deckId }) => {
211
+ try {
212
+ const sessionId = this.getSessionId();
213
+ const deck = await this.fetchDeck(deckId);
214
+ this.sessionBinding.setDeckId(sessionId, deckId);
215
+ const payload = await this.buildBindingPayload(sessionId);
216
+ return {
217
+ content: [{
218
+ type: "text",
219
+ text: JSON.stringify({
220
+ ...payload,
221
+ deck_name: deck.name,
222
+ message: 'Session deck switched. Other sessions and deck.yaml are unchanged.',
223
+ }, null, 2),
224
+ }],
225
+ };
226
+ }
227
+ catch (error) {
228
+ return this.toolError(error);
229
+ }
230
+ });
231
+ this.server.registerTool("get_session_binding", {
232
+ title: "Get Session Binding",
233
+ description: "Show workspace and effective deck for this MCP session, including whether the deck comes from a session override or repo deck.yaml.",
234
+ inputSchema: {},
235
+ }, async () => {
236
+ try {
237
+ const sessionId = this.getSessionId();
238
+ const snapshot = this.sessionBinding.getBinding(sessionId);
239
+ let manifestDeckId;
240
+ if (snapshot.workspaceRoot && !snapshot.deckSource) {
241
+ const manifest = await (0, repo_deck_1.loadRepoDeckManifest)(snapshot.workspaceRoot);
242
+ manifestDeckId = manifest?.deck_id;
243
+ }
244
+ const deck = await this.callBackendAPI('/api/scope/deck');
245
+ return {
246
+ content: [{
247
+ type: "text",
248
+ text: JSON.stringify({
249
+ workspaceRoot: snapshot.workspaceRoot,
250
+ session_deck_override: snapshot.deckId,
251
+ session_deck_source: snapshot.deckSource,
252
+ repo_manifest_deck_id: manifestDeckId,
253
+ effective_deck_id: deck?.id,
254
+ effective_deck_name: deck?.name,
255
+ effective_deck_source: (0, mcp_session_binding_1.resolveDeckBindingSource)(snapshot, manifestDeckId),
256
+ }, null, 2),
257
+ }],
258
+ };
259
+ }
260
+ catch (error) {
261
+ return this.toolError(error);
262
+ }
263
+ });
170
264
  this.server.registerTool("get_repo_deck_status", {
171
265
  title: "Get Repo Deck Status",
172
266
  description: "Check whether .agent-deck/deck.yaml exists in a workspace and whether it links to a valid deck",
@@ -183,7 +277,7 @@ class AgentDeckMCPServer {
183
277
  text: JSON.stringify({
184
278
  status: 'missing',
185
279
  workspaceRoot,
186
- expectedPath: shared_2.REPO_DECK_MANIFEST_PATH,
280
+ expectedPath: shared_1.REPO_DECK_MANIFEST_PATH,
187
281
  message: 'No .agent-deck/deck.yaml found. Use setup_repo_deck to create one.',
188
282
  availableDecks: Array.isArray(decks)
189
283
  ? decks.map((d) => ({ id: d.id, name: d.name }))
@@ -232,7 +326,7 @@ class AgentDeckMCPServer {
232
326
  const sessionId = this.getSessionId();
233
327
  const existing = await (0, repo_deck_1.loadRepoDeckManifest)(workspaceRoot);
234
328
  if (existing) {
235
- this.workspaceBySession.set(sessionId, workspaceRoot);
329
+ this.sessionBinding.setWorkspace(sessionId, workspaceRoot);
236
330
  const resolved = await fetch(`${this.backendUrl}/api/scope/resolve`, {
237
331
  method: 'POST',
238
332
  headers: { ...agentClientHeaders, 'Content-Type': 'application/json' },
@@ -264,7 +358,7 @@ class AgentDeckMCPServer {
264
358
  text: JSON.stringify({
265
359
  status: 'needs_deck_id',
266
360
  workspaceRoot,
267
- expectedPath: shared_2.REPO_DECK_MANIFEST_PATH,
361
+ expectedPath: shared_1.REPO_DECK_MANIFEST_PATH,
268
362
  message: 'Pick a deck id from the dashboard (My Decks → copy icon) and call setup_repo_deck again with deckId.',
269
363
  availableDecks: Array.isArray(decks)
270
364
  ? decks.map((d) => ({ id: d.id, name: d.name }))
@@ -284,7 +378,7 @@ class AgentDeckMCPServer {
284
378
  await promises_1.default.mkdir(dirPath, { recursive: true });
285
379
  await promises_1.default.writeFile(filePath, content, 'utf8');
286
380
  }
287
- this.workspaceBySession.set(sessionId, workspaceRoot);
381
+ this.sessionBinding.setWorkspace(sessionId, workspaceRoot);
288
382
  return {
289
383
  content: [{
290
384
  type: "text",
@@ -328,7 +422,7 @@ class AgentDeckMCPServer {
328
422
  });
329
423
  this.server.registerTool("get_bound_deck", {
330
424
  title: "Get Bound Deck",
331
- description: "Get the deck bound to this session via bind_workspace or AGENT_DECK_WORKSPACE (.agent-deck/deck.yaml)",
425
+ description: "Get the deck bound to this session (session deckId override, env, or .agent-deck/deck.yaml)",
332
426
  inputSchema: {},
333
427
  }, async () => {
334
428
  try {
@@ -1121,7 +1215,7 @@ class AgentDeckMCPServer {
1121
1215
  const sessionId = transport.sessionId;
1122
1216
  if (sessionId) {
1123
1217
  this.sessions.delete(sessionId);
1124
- this.workspaceBySession.delete(sessionId);
1218
+ this.sessionBinding.clearSession(sessionId);
1125
1219
  }
1126
1220
  };
1127
1221
  await server.connect(transport);
@@ -1152,7 +1246,9 @@ class AgentDeckMCPServer {
1152
1246
  this.app.listen(this.port, () => {
1153
1247
  console.log(`✅ Agent Deck MCP Server is ready to accept connections`);
1154
1248
  console.log(`📋 Available tools:`);
1155
- console.log(` - bind_workspace: Bind session to repo via .agent-deck/deck.yaml`);
1249
+ console.log(` - bind_workspace: Bind session to workspace; optional deckId override`);
1250
+ console.log(` - switch_bound_deck: Switch deck for this session only`);
1251
+ console.log(` - get_session_binding: Show session workspace + effective deck`);
1156
1252
  console.log(` - get_repo_deck_status: Check repo deck manifest`);
1157
1253
  console.log(` - setup_repo_deck: Create .agent-deck/deck.yaml for a repo`);
1158
1254
  console.log(` - get_bound_deck: Get workspace-bound deck`);