@decocms/runtime 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -26
- package/package.json +1 -1
- package/src/bindings.ts +4 -1
- package/src/decopilot.ts +8 -2
- package/src/index.ts +1 -0
- package/src/oauth.test.ts +96 -0
- package/src/oauth.ts +47 -2
- package/src/tools.ts +17 -0
- package/src/triggers.ts +1 -1
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ const greetTool = createTool({
|
|
|
39
39
|
|
|
40
40
|
// Create the MCP server
|
|
41
41
|
export default withRuntime({
|
|
42
|
-
tools: [
|
|
42
|
+
tools: [greetTool],
|
|
43
43
|
});
|
|
44
44
|
```
|
|
45
45
|
|
|
@@ -154,21 +154,11 @@ const getUserDataTool = createPrivateTool({
|
|
|
154
154
|
|
|
155
155
|
### Registering Tools
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
Pass an array of tool instances created with `createTool()` / `createPrivateTool()`:
|
|
158
158
|
|
|
159
159
|
```typescript
|
|
160
160
|
export default withRuntime({
|
|
161
|
-
|
|
162
|
-
tools: [
|
|
163
|
-
() => greetTool,
|
|
164
|
-
() => calculateTool,
|
|
165
|
-
(env) => createDynamicTool(env),
|
|
166
|
-
],
|
|
167
|
-
|
|
168
|
-
// Option 2: Single function returning array
|
|
169
|
-
tools: async (env) => {
|
|
170
|
-
return [greetTool, calculateTool];
|
|
171
|
-
},
|
|
161
|
+
tools: [greetTool, calculateTool],
|
|
172
162
|
});
|
|
173
163
|
```
|
|
174
164
|
|
|
@@ -358,10 +348,10 @@ export default withRuntime({
|
|
|
358
348
|
},
|
|
359
349
|
|
|
360
350
|
tools: [
|
|
361
|
-
|
|
351
|
+
createTool({
|
|
362
352
|
id: "query",
|
|
363
353
|
inputSchema: z.object({ sql: z.string() }),
|
|
364
|
-
execute: async ({ runtimeContext }) => {
|
|
354
|
+
execute: async ({ context, runtimeContext }) => {
|
|
365
355
|
// Access resolved bindings from state
|
|
366
356
|
const { database } = runtimeContext.env.MESH_REQUEST_CONTEXT.state;
|
|
367
357
|
return database.QUERY({ sql: context.sql });
|
|
@@ -451,7 +441,7 @@ const channelTools = impl(WellKnownBindings.Channel, [
|
|
|
451
441
|
]);
|
|
452
442
|
|
|
453
443
|
export default withRuntime({
|
|
454
|
-
tools:
|
|
444
|
+
tools: channelTools,
|
|
455
445
|
});
|
|
456
446
|
```
|
|
457
447
|
|
|
@@ -648,16 +638,9 @@ const statusResource = createResource({
|
|
|
648
638
|
|
|
649
639
|
// Export the MCP server
|
|
650
640
|
export default withRuntime({
|
|
651
|
-
tools: [
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
],
|
|
655
|
-
prompts: [
|
|
656
|
-
() => analyzePrompt,
|
|
657
|
-
],
|
|
658
|
-
resources: [
|
|
659
|
-
() => statusResource,
|
|
660
|
-
],
|
|
641
|
+
tools: [echoTool, getProfileTool],
|
|
642
|
+
prompts: [analyzePrompt],
|
|
643
|
+
resources: [statusResource],
|
|
661
644
|
cors: {
|
|
662
645
|
origin: "*",
|
|
663
646
|
credentials: true,
|
package/package.json
CHANGED
package/src/bindings.ts
CHANGED
|
@@ -161,7 +161,10 @@ export const AgentOf = () =>
|
|
|
161
161
|
thinking: AgentModelInfoSchema.optional(),
|
|
162
162
|
coding: AgentModelInfoSchema.optional(),
|
|
163
163
|
fast: AgentModelInfoSchema.optional(),
|
|
164
|
-
toolApprovalLevel: z.enum(["auto", "readonly"
|
|
164
|
+
toolApprovalLevel: z.enum(["auto", "readonly"]).default("auto"),
|
|
165
|
+
mode: z
|
|
166
|
+
.enum(["default", "plan", "web-search", "gen-image"])
|
|
167
|
+
.default("default"),
|
|
165
168
|
temperature: z.number().default(0.5),
|
|
166
169
|
});
|
|
167
170
|
|
package/src/decopilot.ts
CHANGED
|
@@ -31,7 +31,9 @@ export interface AgentBindingConfig {
|
|
|
31
31
|
thinking?: AgentModelInfo;
|
|
32
32
|
coding?: AgentModelInfo;
|
|
33
33
|
fast?: AgentModelInfo;
|
|
34
|
-
toolApprovalLevel?: "auto" | "readonly"
|
|
34
|
+
toolApprovalLevel?: "auto" | "readonly";
|
|
35
|
+
/** Decopilot stream mode — default, plan, web-search, gen-image */
|
|
36
|
+
mode?: "default" | "plan" | "web-search" | "gen-image";
|
|
35
37
|
temperature?: number;
|
|
36
38
|
}
|
|
37
39
|
|
|
@@ -45,7 +47,9 @@ export interface AgentStreamParams {
|
|
|
45
47
|
thinking?: AgentModelInfo;
|
|
46
48
|
coding?: AgentModelInfo;
|
|
47
49
|
fast?: AgentModelInfo;
|
|
48
|
-
toolApprovalLevel?: "auto" | "readonly"
|
|
50
|
+
toolApprovalLevel?: "auto" | "readonly";
|
|
51
|
+
/** Decopilot stream mode — default, plan, web-search, gen-image */
|
|
52
|
+
mode?: "default" | "plan" | "web-search" | "gen-image";
|
|
49
53
|
temperature?: number;
|
|
50
54
|
memory?: { windowSize: number; thread_id: string };
|
|
51
55
|
thread_id?: string;
|
|
@@ -126,6 +130,7 @@ export async function streamAgent(
|
|
|
126
130
|
agent: { id: agentId },
|
|
127
131
|
temperature: params.temperature ?? config.temperature,
|
|
128
132
|
toolApprovalLevel: params.toolApprovalLevel ?? config.toolApprovalLevel,
|
|
133
|
+
mode: params.mode ?? config.mode ?? "default",
|
|
129
134
|
...(params.memory ? { memory: params.memory } : {}),
|
|
130
135
|
...(params.thread_id ? { thread_id: params.thread_id } : {}),
|
|
131
136
|
};
|
|
@@ -188,6 +193,7 @@ export function createDecopilotClient(options: DecopilotClientOptions) {
|
|
|
188
193
|
coding: request.coding,
|
|
189
194
|
fast: request.fast,
|
|
190
195
|
toolApprovalLevel: request.toolApprovalLevel,
|
|
196
|
+
mode: request.mode,
|
|
191
197
|
temperature: request.temperature,
|
|
192
198
|
};
|
|
193
199
|
return streamAgent(streamUrl, token, config, request, opts);
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "./bindings.ts";
|
|
9
9
|
import { type CORSOptions, handlePreflight, withCORS } from "./cors.ts";
|
|
10
10
|
import { createOAuthHandlers } from "./oauth.ts";
|
|
11
|
+
export { OAuthInvalidGrantError } from "./oauth.ts";
|
|
11
12
|
import { State } from "./state.ts";
|
|
12
13
|
import {
|
|
13
14
|
createMCPServer,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import { createOAuthHandlers, OAuthInvalidGrantError } from "./oauth.ts";
|
|
3
|
+
import type { OAuthConfig } from "./tools.ts";
|
|
4
|
+
|
|
5
|
+
const baseConfig = (
|
|
6
|
+
refreshToken?: OAuthConfig["refreshToken"],
|
|
7
|
+
): OAuthConfig => ({
|
|
8
|
+
mode: "PKCE",
|
|
9
|
+
authorizationServer: "https://upstream.example.com",
|
|
10
|
+
authorizationUrl: () => "https://upstream.example.com/authorize",
|
|
11
|
+
exchangeCode: async () => ({
|
|
12
|
+
access_token: "at",
|
|
13
|
+
token_type: "Bearer",
|
|
14
|
+
}),
|
|
15
|
+
refreshToken,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const buildTokenRequest = (body: Record<string, string>) =>
|
|
19
|
+
new Request("https://mcp.example.com/token", {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
22
|
+
body: new URLSearchParams(body).toString(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("OAuth /token refresh handler", () => {
|
|
26
|
+
it("returns 400 invalid_grant when refreshToken throws OAuthInvalidGrantError", async () => {
|
|
27
|
+
const handlers = createOAuthHandlers(
|
|
28
|
+
baseConfig(async () => {
|
|
29
|
+
throw new OAuthInvalidGrantError(
|
|
30
|
+
"invalid_grant",
|
|
31
|
+
"refresh token revoked",
|
|
32
|
+
);
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const response = await handlers.handleToken(
|
|
37
|
+
buildTokenRequest({
|
|
38
|
+
grant_type: "refresh_token",
|
|
39
|
+
refresh_token: "rt",
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(response.status).toBe(400);
|
|
44
|
+
const body = (await response.json()) as {
|
|
45
|
+
error: string;
|
|
46
|
+
error_description?: string;
|
|
47
|
+
};
|
|
48
|
+
expect(body.error).toBe("invalid_grant");
|
|
49
|
+
expect(body.error_description).toBe("refresh token revoked");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("returns 500 server_error when refreshToken throws a generic error", async () => {
|
|
53
|
+
const handlers = createOAuthHandlers(
|
|
54
|
+
baseConfig(async () => {
|
|
55
|
+
throw new Error("upstream is down");
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const response = await handlers.handleToken(
|
|
60
|
+
buildTokenRequest({
|
|
61
|
+
grant_type: "refresh_token",
|
|
62
|
+
refresh_token: "rt",
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(response.status).toBe(500);
|
|
67
|
+
const body = (await response.json()) as { error: string };
|
|
68
|
+
expect(body.error).toBe("server_error");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("forwards the new token on success", async () => {
|
|
72
|
+
const handlers = createOAuthHandlers(
|
|
73
|
+
baseConfig(async () => ({
|
|
74
|
+
access_token: "fresh",
|
|
75
|
+
token_type: "Bearer",
|
|
76
|
+
refresh_token: "rt2",
|
|
77
|
+
expires_in: 3600,
|
|
78
|
+
})),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const response = await handlers.handleToken(
|
|
82
|
+
buildTokenRequest({
|
|
83
|
+
grant_type: "refresh_token",
|
|
84
|
+
refresh_token: "rt",
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(response.status).toBe(200);
|
|
89
|
+
const body = (await response.json()) as {
|
|
90
|
+
access_token: string;
|
|
91
|
+
refresh_token?: string;
|
|
92
|
+
};
|
|
93
|
+
expect(body.access_token).toBe("fresh");
|
|
94
|
+
expect(body.refresh_token).toBe("rt2");
|
|
95
|
+
});
|
|
96
|
+
});
|
package/src/oauth.ts
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import type { OAuthClient, OAuthConfig, OAuthParams } from "./tools.ts";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Thrown by `OAuthConfig.refreshToken` (or `exchangeCode`) implementations
|
|
5
|
+
* when the upstream OAuth provider says the grant itself is permanently
|
|
6
|
+
* invalid — e.g. GitHub returns `400 invalid_grant` because the user
|
|
7
|
+
* revoked the app or the refresh_token was rotated out from under us.
|
|
8
|
+
*
|
|
9
|
+
* The `/token` handler maps this to an RFC-6749-compliant
|
|
10
|
+
* `400 {"error":"invalid_grant",...}` response, so callers can tell apart
|
|
11
|
+
* "the user needs to reconnect" from a transient upstream 5xx (which the
|
|
12
|
+
* outer catch maps to a 500). Throwing a plain `Error` from `refreshToken`
|
|
13
|
+
* will be treated as transient and surface as 500.
|
|
14
|
+
*/
|
|
15
|
+
export class OAuthInvalidGrantError extends Error {
|
|
16
|
+
readonly error: string;
|
|
17
|
+
readonly errorDescription?: string;
|
|
18
|
+
constructor(error = "invalid_grant", errorDescription?: string) {
|
|
19
|
+
super(errorDescription ?? error);
|
|
20
|
+
this.name = "OAuthInvalidGrantError";
|
|
21
|
+
this.error = error;
|
|
22
|
+
this.errorDescription = errorDescription;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
3
26
|
/**
|
|
4
27
|
* Generate a cryptographically secure random token
|
|
5
28
|
*/
|
|
@@ -338,8 +361,30 @@ export function createOAuthHandlers(oauth: OAuthConfig) {
|
|
|
338
361
|
);
|
|
339
362
|
}
|
|
340
363
|
|
|
341
|
-
// Call the external provider to refresh the token
|
|
342
|
-
|
|
364
|
+
// Call the external provider to refresh the token. We catch
|
|
365
|
+
// `OAuthInvalidGrantError` here (not in the outer catch) so we can
|
|
366
|
+
// map it to a spec-compliant 400 instead of letting all errors fall
|
|
367
|
+
// through to a generic 500. Any other thrown error is treated as
|
|
368
|
+
// transient and surfaces from the outer catch as 500.
|
|
369
|
+
let newTokenResponse: Awaited<
|
|
370
|
+
ReturnType<NonNullable<OAuthConfig["refreshToken"]>>
|
|
371
|
+
>;
|
|
372
|
+
try {
|
|
373
|
+
newTokenResponse = await oauth.refreshToken(refresh_token);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
if (err instanceof OAuthInvalidGrantError) {
|
|
376
|
+
return Response.json(
|
|
377
|
+
{
|
|
378
|
+
error: err.error,
|
|
379
|
+
...(err.errorDescription
|
|
380
|
+
? { error_description: err.errorDescription }
|
|
381
|
+
: {}),
|
|
382
|
+
},
|
|
383
|
+
{ status: 400 },
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
throw err;
|
|
387
|
+
}
|
|
343
388
|
|
|
344
389
|
const tokenResponse: Record<string, unknown> = {
|
|
345
390
|
access_token: newTokenResponse.access_token,
|
package/src/tools.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
9
|
import { WebStandardStreamableHTTPServerTransport as HttpServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
10
10
|
import type {
|
|
11
|
+
CallToolResult,
|
|
11
12
|
GetPromptResult,
|
|
12
13
|
Implementation,
|
|
13
14
|
ToolAnnotations,
|
|
@@ -957,6 +958,22 @@ export const createMCPServer = <
|
|
|
957
958
|
ctx,
|
|
958
959
|
);
|
|
959
960
|
|
|
961
|
+
if (
|
|
962
|
+
result != null &&
|
|
963
|
+
typeof result === "object" &&
|
|
964
|
+
"content" in result &&
|
|
965
|
+
Array.isArray(result.content) &&
|
|
966
|
+
result.content.every(
|
|
967
|
+
(item: unknown) =>
|
|
968
|
+
item != null &&
|
|
969
|
+
typeof item === "object" &&
|
|
970
|
+
"type" in item &&
|
|
971
|
+
typeof (item as Record<string, unknown>).type === "string",
|
|
972
|
+
)
|
|
973
|
+
) {
|
|
974
|
+
return result as CallToolResult;
|
|
975
|
+
}
|
|
976
|
+
|
|
960
977
|
return {
|
|
961
978
|
structuredContent: result as Record<string, unknown>,
|
|
962
979
|
content: [
|