@decocms/mesh-sdk 1.2.2 → 1.2.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.
@@ -1,4 +1,8 @@
1
- import type { AiProviderModel, ProviderId } from "../types/ai-providers";
1
+ import type {
2
+ AiProviderModel,
3
+ AiProviderKey,
4
+ ProviderId,
5
+ } from "../types/ai-providers";
2
6
 
3
7
  /**
4
8
  * Preferred default models for each well-known provider.
@@ -11,8 +15,7 @@ export const DEFAULT_MODEL_PREFERENCES: Partial<Record<ProviderId, string[]>> =
11
15
  {
12
16
  anthropic: ["claude-sonnet-4-6", "claude-sonnet", "claude"],
13
17
  openrouter: [
14
- "anthropic/claude-opus-4.6",
15
- "anthropic/claude-4.6-opus",
18
+ "anthropic/claude-opus-4.8",
16
19
  "anthropic/claude-sonnet-4-6",
17
20
  "anthropic/claude-sonnet",
18
21
  "anthropic/claude",
@@ -28,6 +31,7 @@ export const DEFAULT_MODEL_PREFERENCES: Partial<Record<ProviderId, string[]>> =
28
31
  "claude-code:opus",
29
32
  "claude-code:haiku",
30
33
  ],
34
+ codex: ["codex:gpt-5.4"],
31
35
  };
32
36
 
33
37
  /**
@@ -44,6 +48,8 @@ export const FAST_MODEL_PREFERENCES: Partial<Record<ProviderId, string[]>> = {
44
48
  ],
45
49
  deco: ["qwen/qwen3.5-flash", "anthropic/claude-haiku"],
46
50
  google: ["gemini-2.5-flash", "gemini-3-flash"],
51
+ "claude-code": ["claude-code:haiku", "claude-code:sonnet"],
52
+ codex: ["codex:gpt-5.4-mini"],
47
53
  };
48
54
 
49
55
  /**
@@ -55,6 +61,185 @@ export function getFastModel(providerId: ProviderId): string | null {
55
61
  return candidates?.[0] ?? null;
56
62
  }
57
63
 
64
+ /**
65
+ * Preferred smart (balanced) models per provider — used as the "Smart" tier
66
+ * in Simple Model Mode.
67
+ */
68
+ export const SMART_MODEL_PREFERENCES: Partial<Record<ProviderId, string[]>> = {
69
+ anthropic: ["claude-sonnet-4-6", "claude-sonnet"],
70
+ openrouter: [
71
+ "anthropic/claude-sonnet-4.6",
72
+ "anthropic/claude-sonnet",
73
+ "anthropic/claude-opus-4.8",
74
+ "google/gemini-3-pro",
75
+ ],
76
+ deco: [
77
+ "anthropic/claude-sonnet-4.6",
78
+ "anthropic/claude-sonnet",
79
+ "anthropic/claude",
80
+ ],
81
+ google: ["gemini-3-pro", "gemini-3-flash"],
82
+ "claude-code": ["claude-code:sonnet"],
83
+ codex: ["codex:gpt-5.4"],
84
+ };
85
+
86
+ /**
87
+ * Preferred thinking/reasoning models per provider — used as the "Thinking" tier
88
+ * in Simple Model Mode.
89
+ */
90
+ export const THINKING_MODEL_PREFERENCES: Partial<Record<ProviderId, string[]>> =
91
+ {
92
+ anthropic: ["claude-opus-4-8", "claude-sonnet-4-6", "claude-sonnet"],
93
+ openrouter: [
94
+ "anthropic/claude-opus-4.8",
95
+ "anthropic/claude-sonnet-4.6:extended",
96
+ "anthropic/claude-sonnet-4.6",
97
+ "google/gemini-3-pro",
98
+ ],
99
+ deco: [
100
+ "anthropic/claude-opus",
101
+ "anthropic/claude-sonnet-4.6",
102
+ "anthropic/claude-sonnet",
103
+ ],
104
+ google: ["gemini-3-pro"],
105
+ "claude-code": ["claude-code:opus", "claude-code:sonnet"],
106
+ codex: ["codex:gpt-5.5"],
107
+ };
108
+
109
+ /**
110
+ * Preferred image generation models per provider.
111
+ * Falls back to first model with "image" capability.
112
+ */
113
+ export const IMAGE_MODEL_PREFERENCES: Partial<Record<ProviderId, string[]>> = {
114
+ openrouter: ["openai/gpt-image-1", "google/gemini-2.0-flash-image"],
115
+ deco: ["openai/gpt-image-1", "google/gemini-2.0-flash-image"],
116
+ google: ["gemini-2.0-flash-image"],
117
+ };
118
+
119
+ /**
120
+ * Preferred web research models per provider.
121
+ * Falls back to first model whose id includes "sonar" or "deepresearch".
122
+ */
123
+ export const WEB_RESEARCH_MODEL_PREFERENCES: Partial<
124
+ Record<ProviderId, string[]>
125
+ > = {
126
+ openrouter: [
127
+ "perplexity/sonar",
128
+ "perplexity/sonar-pro",
129
+ "perplexity/deep-research",
130
+ ],
131
+ deco: [
132
+ "perplexity/sonar",
133
+ "perplexity/sonar-pro",
134
+ "perplexity/deep-research",
135
+ ],
136
+ };
137
+
138
+ export interface SimpleModeModelSlot {
139
+ keyId: string;
140
+ modelId: string;
141
+ title?: string;
142
+ }
143
+
144
+ export interface SimpleModeDefaults {
145
+ chat: {
146
+ fast: SimpleModeModelSlot | null;
147
+ smart: SimpleModeModelSlot | null;
148
+ thinking: SimpleModeModelSlot | null;
149
+ };
150
+ image: SimpleModeModelSlot | null;
151
+ webResearch: SimpleModeModelSlot | null;
152
+ }
153
+
154
+ function resolveSlot(
155
+ models: AiProviderModel[],
156
+ keyId: string,
157
+ preferences: string[],
158
+ fallback?: (m: AiProviderModel) => boolean,
159
+ ): SimpleModeModelSlot | null {
160
+ for (const candidate of preferences) {
161
+ const exact = models.find((m) => m.modelId === candidate);
162
+ if (exact) return { keyId, modelId: exact.modelId, title: exact.title };
163
+ }
164
+ for (const candidate of preferences) {
165
+ const partial = models.find((m) => m.modelId.includes(candidate));
166
+ if (partial)
167
+ return { keyId, modelId: partial.modelId, title: partial.title };
168
+ }
169
+ if (fallback) {
170
+ const found = models.find(fallback);
171
+ if (found) return { keyId, modelId: found.modelId, title: found.title };
172
+ }
173
+ return null;
174
+ }
175
+
176
+ /**
177
+ * Compute sensible Simple Mode defaults from the currently-connected keys and
178
+ * their available models. Each slot picks the best candidate per the tier
179
+ * preference lists, falling back to capability-based detection for image/web.
180
+ *
181
+ * @param keys The org's connected AI provider keys.
182
+ * @param modelsByKeyId Map of keyId → available model list.
183
+ */
184
+ export function pickSimpleModeDefaults(
185
+ keys: AiProviderKey[],
186
+ modelsByKeyId: Record<string, AiProviderModel[]>,
187
+ ): SimpleModeDefaults {
188
+ const result: SimpleModeDefaults = {
189
+ chat: { fast: null, smart: null, thinking: null },
190
+ image: null,
191
+ webResearch: null,
192
+ };
193
+
194
+ for (const key of keys) {
195
+ const models = modelsByKeyId[key.id] ?? [];
196
+ const providerId = key.providerId as ProviderId;
197
+
198
+ if (!result.chat.fast) {
199
+ result.chat.fast = resolveSlot(
200
+ models,
201
+ key.id,
202
+ FAST_MODEL_PREFERENCES[providerId] ?? [],
203
+ );
204
+ }
205
+ if (!result.chat.smart) {
206
+ result.chat.smart = resolveSlot(
207
+ models,
208
+ key.id,
209
+ SMART_MODEL_PREFERENCES[providerId] ?? [],
210
+ );
211
+ }
212
+ if (!result.chat.thinking) {
213
+ result.chat.thinking = resolveSlot(
214
+ models,
215
+ key.id,
216
+ THINKING_MODEL_PREFERENCES[providerId] ?? [],
217
+ );
218
+ }
219
+ if (!result.image) {
220
+ result.image = resolveSlot(
221
+ models,
222
+ key.id,
223
+ IMAGE_MODEL_PREFERENCES[providerId] ?? [],
224
+ (m) => m.capabilities?.includes("image") === true,
225
+ );
226
+ }
227
+ if (!result.webResearch) {
228
+ result.webResearch = resolveSlot(
229
+ models,
230
+ key.id,
231
+ WEB_RESEARCH_MODEL_PREFERENCES[providerId] ?? [],
232
+ (m) => {
233
+ const n = m.modelId.toLowerCase().replace(/[^a-z0-9]/g, "");
234
+ return n.includes("sonar") || n.includes("deepresearch");
235
+ },
236
+ );
237
+ }
238
+ }
239
+
240
+ return result;
241
+ }
242
+
58
243
  /**
59
244
  * Select the best default model from a loaded list for a given provider.
60
245
  *
@@ -258,6 +258,10 @@ export interface OAuthTokenInfo {
258
258
  clientId: string | null;
259
259
  clientSecret: string | null;
260
260
  tokenEndpoint: string | null;
261
+ /** OIDC ID token (JWT) returned by some providers (e.g. Google). Contains user identity claims like email. */
262
+ idToken: string | null;
263
+ /** OIDC userinfo endpoint URL from authorization server metadata. Can be called with the access token to retrieve user identity. */
264
+ userinfoEndpoint: string | null;
261
265
  }
262
266
 
263
267
  /**
@@ -278,6 +282,7 @@ interface FullTokenResult {
278
282
  clientId: string | null;
279
283
  clientSecret: string | null;
280
284
  tokenEndpoint: string | null;
285
+ userinfoEndpoint: string | null;
281
286
  }
282
287
 
283
288
  /**
@@ -293,6 +298,8 @@ interface FullTokenResult {
293
298
  */
294
299
  export async function authenticateMcp(params: {
295
300
  connectionId: string;
301
+ /** Organization slug — used to build the org-scoped /api/:org/mcp/... URL. */
302
+ orgSlug?: string;
296
303
  /** Mesh server URL - optional, defaults to window.location.origin (for external apps, provide your Mesh server URL) */
297
304
  meshUrl?: string;
298
305
  clientName?: string;
@@ -305,7 +312,10 @@ export async function authenticateMcp(params: {
305
312
  windowMode?: OAuthWindowMode;
306
313
  }): Promise<AuthenticateMcpResult> {
307
314
  const baseUrl = params.meshUrl ?? window.location.origin;
308
- const serverUrl = new URL(`/mcp/${params.connectionId}`, baseUrl);
315
+ const path = params.orgSlug
316
+ ? `/api/${encodeURIComponent(params.orgSlug)}/mcp/${params.connectionId}`
317
+ : `/mcp/${params.connectionId}`;
318
+ const serverUrl = new URL(path, baseUrl);
309
319
  const provider = new McpOAuthProvider({
310
320
  serverUrl: serverUrl.href,
311
321
  clientName: params.clientName,
@@ -424,6 +434,11 @@ export async function authenticateMcp(params: {
424
434
  ? (clientInfo.client_secret as string)
425
435
  : null,
426
436
  tokenEndpoint: authServerMetadata?.token_endpoint ?? null,
437
+ userinfoEndpoint:
438
+ (authServerMetadata?.userinfo_endpoint as
439
+ | string
440
+ | null
441
+ | undefined) ?? null,
427
442
  });
428
443
  } catch (err) {
429
444
  cleanup();
@@ -473,6 +488,7 @@ export async function authenticateMcp(params: {
473
488
 
474
489
  if (result === "REDIRECT") {
475
490
  const fullResult = await oauthCompletePromise;
491
+ const rawTokens = fullResult.tokens as unknown as Record<string, unknown>;
476
492
  return {
477
493
  token: fullResult.tokens.access_token,
478
494
  tokenInfo: {
@@ -483,6 +499,9 @@ export async function authenticateMcp(params: {
483
499
  clientId: fullResult.clientId,
484
500
  clientSecret: fullResult.clientSecret,
485
501
  tokenEndpoint: fullResult.tokenEndpoint,
502
+ userinfoEndpoint: fullResult.userinfoEndpoint,
503
+ idToken:
504
+ typeof rawTokens.id_token === "string" ? rawTokens.id_token : null,
486
505
  },
487
506
  error: null,
488
507
  };
@@ -491,6 +510,7 @@ export async function authenticateMcp(params: {
491
510
  // If we got here without redirect, check for tokens
492
511
  const tokens = provider.tokens();
493
512
  const clientInfo = provider.clientInformation();
513
+ const rawTokens = tokens as unknown as Record<string, unknown> | null;
494
514
  return {
495
515
  token: tokens?.access_token || null,
496
516
  tokenInfo: tokens
@@ -505,6 +525,11 @@ export async function authenticateMcp(params: {
505
525
  ? (clientInfo.client_secret as string)
506
526
  : null,
507
527
  tokenEndpoint: null, // Would need to be passed through
528
+ userinfoEndpoint: null,
529
+ idToken:
530
+ rawTokens && typeof rawTokens.id_token === "string"
531
+ ? rawTokens.id_token
532
+ : null,
508
533
  }
509
534
  : null,
510
535
  error: null,
@@ -680,14 +705,32 @@ function getCurrentOrigin(): string | undefined {
680
705
  }
681
706
 
682
707
  /**
683
- * Extract connection ID from MCP proxy URL
708
+ * Extract connection ID from MCP proxy URL.
709
+ * Supports both legacy `/mcp/:id` and org-scoped `/api/:org/mcp/:id` paths.
684
710
  */
685
711
  function extractConnectionIdFromUrl(url: string): string | null {
686
712
  try {
687
713
  // Use current origin as base for relative URLs (browser only)
688
714
  const base = getCurrentOrigin();
689
715
  const urlObj = base ? new URL(url, base) : new URL(url);
690
- const match = urlObj.pathname.match(/^\/mcp\/([^/]+)/);
716
+ const orgScoped = urlObj.pathname.match(/^\/api\/[^/]+\/mcp\/([^/]+)/);
717
+ if (orgScoped) return orgScoped[1] ?? null;
718
+ const legacy = urlObj.pathname.match(/^\/mcp\/([^/]+)/);
719
+ return legacy?.[1] ?? null;
720
+ } catch {
721
+ return null;
722
+ }
723
+ }
724
+
725
+ /**
726
+ * Extract org slug from an org-scoped MCP proxy URL (`/api/:org/mcp/...`).
727
+ * Returns null for legacy `/mcp/...` URLs.
728
+ */
729
+ function extractOrgSlugFromUrl(url: string): string | null {
730
+ try {
731
+ const base = getCurrentOrigin();
732
+ const urlObj = base ? new URL(url, base) : new URL(url);
733
+ const match = urlObj.pathname.match(/^\/api\/([^/]+)\/mcp\//);
691
734
  return match?.[1] ?? null;
692
735
  } catch {
693
736
  return null;
@@ -697,14 +740,16 @@ function extractConnectionIdFromUrl(url: string): string | null {
697
740
  /**
698
741
  * Check if connection has a stored OAuth token
699
742
  * @param connectionId - The connection ID to check
743
+ * @param orgSlug - Organization slug used to build the org-scoped path
700
744
  * @param apiBaseUrl - Base URL for the API call (optional, defaults to relative path)
701
745
  */
702
746
  async function checkOAuthTokenStatus(
703
747
  connectionId: string,
748
+ orgSlug: string,
704
749
  apiBaseUrl?: string,
705
750
  ): Promise<{ hasToken: boolean }> {
706
751
  try {
707
- const path = `/api/connections/${connectionId}/oauth-token/status`;
752
+ const path = `/api/${encodeURIComponent(orgSlug)}/connections/${connectionId}/oauth-token/status`;
708
753
  const url = apiBaseUrl ? new URL(path, apiBaseUrl).href : path;
709
754
  const currentOrigin = getCurrentOrigin();
710
755
  const isSameOrigin =
@@ -724,17 +769,21 @@ async function checkOAuthTokenStatus(
724
769
 
725
770
  /**
726
771
  * Check if an MCP connection is authenticated and whether it supports OAuth
727
- * @param params.url - The MCP URL to check
772
+ * @param params.url - The org-scoped MCP URL to check (`/api/:org/mcp/...`)
728
773
  * @param params.token - Authorization token (optional)
774
+ * @param params.orgId - Organization ID (deprecated; org is now resolved from the URL path)
729
775
  * @param params.meshUrl - Mesh server URL for API calls (optional, defaults to URL origin)
730
776
  */
731
777
  export async function isConnectionAuthenticated({
732
778
  url,
733
779
  token,
780
+ orgId: _orgId,
734
781
  meshUrl,
735
782
  }: {
736
783
  url: string;
737
784
  token: string | null;
785
+ /** @deprecated Org is resolved from the URL path; this is kept for call-site compatibility. */
786
+ orgId?: string;
738
787
  /** Mesh server URL for API calls - optional, defaults to extracting from url parameter */
739
788
  meshUrl?: string;
740
789
  }): Promise<McpAuthStatus> {
@@ -766,6 +815,7 @@ export async function isConnectionAuthenticated({
766
815
 
767
816
  // Extract connection ID for OAuth token status check
768
817
  const connectionId = extractConnectionIdFromUrl(url);
818
+ const orgSlug = extractOrgSlugFromUrl(url);
769
819
  // Determine base URL for API calls (meshUrl > URL origin > current origin)
770
820
  // Use current origin as base for relative URLs (browser only)
771
821
  const base = getCurrentOrigin();
@@ -774,9 +824,10 @@ export async function isConnectionAuthenticated({
774
824
 
775
825
  if (response.ok) {
776
826
  // Check if we have an OAuth token stored for this connection
777
- const oauthStatus = connectionId
778
- ? await checkOAuthTokenStatus(connectionId, apiBaseUrl)
779
- : { hasToken: false };
827
+ const oauthStatus =
828
+ connectionId && orgSlug
829
+ ? await checkOAuthTokenStatus(connectionId, orgSlug, apiBaseUrl)
830
+ : { hasToken: false };
780
831
 
781
832
  return {
782
833
  isAuthenticated: true,
@@ -15,10 +15,10 @@ export const KEYS = {
15
15
  threads: (locator: string) => ["threads", locator] as const,
16
16
  virtualMcpThreads: (locator: string, virtualMcpId: string) =>
17
17
  ["threads", locator, "virtual-mcp", virtualMcpId] as const,
18
- thread: (locator: string, threadId: string) =>
19
- ["thread", locator, threadId] as const,
20
- threadMessages: (locator: string, threadId: string) =>
21
- ["thread-messages", locator, threadId] as const,
18
+ thread: (locator: string, taskId: string) =>
19
+ ["thread", locator, taskId] as const,
20
+ threadMessages: (locator: string, taskId: string) =>
21
+ ["thread-messages", locator, taskId] as const,
22
22
  messages: (locator: string) => ["messages", locator] as const,
23
23
 
24
24
  // Organizations list
@@ -81,6 +81,10 @@ export const KEYS = {
81
81
  // Models list (scoped by organization)
82
82
  modelsList: (orgId: string) => ["models-list", orgId] as const,
83
83
 
84
+ // Virtual MCP last-used info (most recent thread per agent)
85
+ virtualMcpLastUsed: (orgId: string, ids: string[]) =>
86
+ ["virtual-mcp", "last-used", orgId, ids] as const,
87
+
84
88
  // Collections (scoped by connection)
85
89
  connectionCollections: (connectionId: string) =>
86
90
  [connectionId, "collections", "discovery"] as const,
@@ -149,6 +153,13 @@ export const KEYS = {
149
153
  owner: string | null | undefined,
150
154
  repo: string | null | undefined,
151
155
  ) => ["github-readme", owner, repo] as const,
156
+ githubBranches: (
157
+ orgId: string,
158
+ orgSlug: string,
159
+ connectionId: string | null | undefined,
160
+ owner: string,
161
+ repo: string,
162
+ ) => ["github-branches", orgId, orgSlug, connectionId, owner, repo] as const,
152
163
 
153
164
  // Monitoring queries
154
165
  monitoringStats: () => ["monitoring", "stats"] as const,
@@ -176,4 +187,8 @@ export const KEYS = {
176
187
 
177
188
  // User data
178
189
  user: (userId: string) => ["user", userId] as const,
190
+
191
+ // Registry app lookup (by app ID)
192
+ registryApp: (orgId: string, appId: string) =>
193
+ ["registry-app", orgId, appId] as const,
179
194
  } as const;
@@ -1,146 +1,4 @@
1
- /**
2
- * Server-Client Bridge
3
- *
4
- * Creates an MCP Server that delegates all requests to an MCP Client.
5
- * This allows using a Client as if it were a Server, useful for proxying
6
- * or bridging between different transport layers.
7
- *
8
- * ## Usage
9
- *
10
- * ```ts
11
- * import { createServerFromClient } from "@decocms/mesh-sdk";
12
- * import { Client } from "@modelcontextprotocol/sdk/client/index.js";
13
- * import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
14
- *
15
- * const client = new Client(...);
16
- * await client.connect(clientTransport);
17
- *
18
- * const server = createServerFromClient(
19
- * client,
20
- * { name: "proxy-server", version: "1.0.0" }
21
- * );
22
- *
23
- * const transport = new WebStandardStreamableHTTPServerTransport({});
24
- * await server.connect(transport);
25
- *
26
- * // Handle requests via transport.handleRequest(req)
27
- * ```
28
- */
29
-
30
- import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
31
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
32
- import type {
33
- Implementation,
34
- ServerCapabilities,
35
- } from "@modelcontextprotocol/sdk/types.js";
36
- import {
37
- CallToolRequestSchema,
38
- GetPromptRequestSchema,
39
- ListPromptsRequestSchema,
40
- ListResourcesRequestSchema,
41
- ListResourceTemplatesRequestSchema,
42
- ListToolsRequestSchema,
43
- ReadResourceRequestSchema,
44
- } from "@modelcontextprotocol/sdk/types.js";
45
-
46
- /**
47
- * Options for creating a server from a client
48
- */
49
- export interface ServerFromClientOptions {
50
- /**
51
- * Server capabilities. If not provided, uses client.getServerCapabilities()
52
- */
53
- capabilities?: ServerCapabilities;
54
- /**
55
- * Server instructions. If not provided, uses client.getInstructions()
56
- */
57
- instructions?: string;
58
- /**
59
- * Timeout in milliseconds for tool calls forwarded to the client.
60
- * If not provided, the MCP SDK default (60s) is used.
61
- */
62
- toolCallTimeoutMs?: number;
63
- }
64
-
65
- /**
66
- * Creates an MCP Server that delegates all requests to the provided Client.
67
- *
68
- * @param client - The MCP Client to delegate requests to
69
- * @param serverInfo - Server metadata (ImplementationSchema-compatible: name, version, title, description, icons, websiteUrl)
70
- * @param options - Optional server configuration (capabilities and instructions)
71
- * @returns An MCP Server instance configured to delegate to the client
72
- */
73
- export function createServerFromClient(
74
- client: Client,
75
- serverInfo: Implementation,
76
- options?: ServerFromClientOptions,
77
- ): McpServer {
78
- // Get capabilities from client if not provided
79
- const capabilities = options?.capabilities ?? client.getServerCapabilities();
80
-
81
- // Get instructions from client if not provided
82
- const instructions = options?.instructions ?? client.getInstructions();
83
-
84
- // Create MCP server with capabilities and instructions
85
- const server = new McpServer(serverInfo, {
86
- capabilities,
87
- instructions,
88
- });
89
-
90
- // Set up request handlers that delegate to client methods
91
-
92
- // Tools handlers
93
- // Strip outputSchema from tools so downstream clients (e.g. the browser's
94
- // MCP Client) don't cache validators and reject structuredContent that
95
- // doesn't perfectly match the downstream server's declared schema.
96
- // A proxy should pass through responses as-is — validation is the
97
- // responsibility of the originating server, not intermediaries.
98
- server.server.setRequestHandler(ListToolsRequestSchema, async () => {
99
- const result = await client.listTools();
100
- return {
101
- ...result,
102
- tools: result.tools.map(({ outputSchema: _, ...tool }) => tool),
103
- };
104
- });
105
-
106
- server.server.setRequestHandler(CallToolRequestSchema, (request) =>
107
- client.callTool(
108
- request.params,
109
- undefined,
110
- options?.toolCallTimeoutMs
111
- ? { timeout: options.toolCallTimeoutMs }
112
- : undefined,
113
- ),
114
- );
115
-
116
- // Resources handlers (only if capabilities include resources)
117
- if (capabilities?.resources) {
118
- server.server.setRequestHandler(ListResourcesRequestSchema, () =>
119
- client.listResources(),
120
- );
121
-
122
- server.server.setRequestHandler(ReadResourceRequestSchema, (request) =>
123
- client.readResource(request.params),
124
- );
125
-
126
- server.server.setRequestHandler(ListResourceTemplatesRequestSchema, () =>
127
- client.listResourceTemplates(),
128
- );
129
- }
130
-
131
- // Prompts handlers (only if capabilities include prompts)
132
- if (capabilities?.prompts) {
133
- server.server.setRequestHandler(ListPromptsRequestSchema, () =>
134
- client.listPrompts(),
135
- );
136
-
137
- server.server.setRequestHandler(GetPromptRequestSchema, (request) =>
138
- client.getPrompt({
139
- ...request.params,
140
- arguments: request.params.arguments ?? {},
141
- }),
142
- );
143
- }
144
-
145
- return server;
146
- }
1
+ export {
2
+ createServerFromClient,
3
+ type ServerFromClientOptions,
4
+ } from "@decocms/mcp-utils";
@@ -160,4 +160,70 @@ describe("addUsage", () => {
160
160
  const result = addUsage(acc, null);
161
161
  expect(result).toBe(acc);
162
162
  });
163
+
164
+ test("accumulates cache tokens from inputTokenDetails", () => {
165
+ const acc = emptyUsageStats();
166
+ const step = {
167
+ inputTokens: 1000,
168
+ outputTokens: 50,
169
+ inputTokenDetails: {
170
+ cacheReadTokens: 800,
171
+ cacheWriteTokens: 100,
172
+ },
173
+ };
174
+ const result = addUsage(acc, step);
175
+ expect(result.cacheReadTokens).toBe(800);
176
+ expect(result.cacheWriteTokens).toBe(100);
177
+ });
178
+
179
+ test("falls back to cachedInputTokens when inputTokenDetails is absent", () => {
180
+ const acc = emptyUsageStats();
181
+ const step = {
182
+ inputTokens: 1000,
183
+ outputTokens: 50,
184
+ cachedInputTokens: 600,
185
+ };
186
+ const result = addUsage(acc, step);
187
+ expect(result.cacheReadTokens).toBe(600);
188
+ expect(result.cacheWriteTokens).toBe(0);
189
+ });
190
+
191
+ test("inputTokenDetails takes precedence over cachedInputTokens", () => {
192
+ const acc = emptyUsageStats();
193
+ const step = {
194
+ inputTokens: 1000,
195
+ cachedInputTokens: 999, // would be wrong
196
+ inputTokenDetails: { cacheReadTokens: 800, cacheWriteTokens: 0 },
197
+ };
198
+ const result = addUsage(acc, step);
199
+ expect(result.cacheReadTokens).toBe(800);
200
+ });
201
+ });
202
+
203
+ describe("calculateUsageStats — cache fields", () => {
204
+ test("sums cache read/write across messages", () => {
205
+ const messages = [
206
+ {
207
+ metadata: {
208
+ usage: {
209
+ inputTokens: 100,
210
+ outputTokens: 50,
211
+ inputTokenDetails: { cacheReadTokens: 80, cacheWriteTokens: 10 },
212
+ },
213
+ },
214
+ },
215
+ {
216
+ metadata: {
217
+ usage: {
218
+ inputTokens: 200,
219
+ outputTokens: 25,
220
+ inputTokenDetails: { cacheReadTokens: 150 },
221
+ },
222
+ },
223
+ },
224
+ ];
225
+ const result = calculateUsageStats(messages);
226
+ expect(result.cacheReadTokens).toBe(230);
227
+ expect(result.cacheWriteTokens).toBe(10);
228
+ });
163
229
  });