@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.
- package/README.md +10 -10
- package/package.json +3 -2
- package/src/context/index.ts +0 -1
- package/src/context/project-context.tsx +0 -10
- package/src/hooks/index.ts +3 -0
- package/src/hooks/use-collections.ts +19 -12
- package/src/hooks/use-connection.ts +12 -1
- package/src/hooks/use-mcp-client.ts +27 -10
- package/src/hooks/use-virtual-mcp.ts +64 -0
- package/src/index.ts +38 -2
- package/src/lib/bridge-transport.ts +6 -434
- package/src/lib/constants.test.ts +26 -0
- package/src/lib/constants.ts +121 -67
- package/src/lib/default-model.ts +188 -3
- package/src/lib/mcp-oauth.ts +59 -8
- package/src/lib/query-keys.ts +19 -4
- package/src/lib/server-client-bridge.ts +4 -146
- package/src/lib/usage.test.ts +66 -0
- package/src/lib/usage.ts +26 -0
- package/src/types/ai-providers.ts +19 -1
- package/src/types/connection.ts +5 -0
- package/src/types/decopilot-events.test.ts +78 -0
- package/src/types/decopilot-events.ts +51 -8
- package/src/types/index.ts +18 -0
- package/src/types/virtual-mcp.test.ts +202 -0
- package/src/types/virtual-mcp.ts +416 -9
package/src/lib/default-model.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
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.
|
|
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
|
*
|
package/src/lib/mcp-oauth.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
778
|
-
|
|
779
|
-
|
|
827
|
+
const oauthStatus =
|
|
828
|
+
connectionId && orgSlug
|
|
829
|
+
? await checkOAuthTokenStatus(connectionId, orgSlug, apiBaseUrl)
|
|
830
|
+
: { hasToken: false };
|
|
780
831
|
|
|
781
832
|
return {
|
|
782
833
|
isAuthenticated: true,
|
package/src/lib/query-keys.ts
CHANGED
|
@@ -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,
|
|
19
|
-
["thread", locator,
|
|
20
|
-
threadMessages: (locator: string,
|
|
21
|
-
["thread-messages", locator,
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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";
|
package/src/lib/usage.test.ts
CHANGED
|
@@ -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
|
});
|