skybridge 2.0.0-beta.26f126f → 2.0.0-beta.33c6f26
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/dist/server/app.d.ts +96 -39
- package/dist/server/app.js +77 -48
- package/dist/server/app.js.map +1 -1
- package/dist/server/app.test.d.ts +1 -0
- package/dist/server/app.test.js +97 -0
- package/dist/server/app.test.js.map +1 -0
- package/dist/server/auth/discovery.d.ts +1 -1
- package/dist/server/auth/discovery.js +1 -1
- package/dist/server/auth/discovery.js.map +1 -1
- package/dist/server/auth/index.d.ts +1 -1
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/auth/setup.test.js +56 -51
- package/dist/server/auth/setup.test.js.map +1 -1
- package/dist/server/auth-extra.test-d.js +91 -35
- package/dist/server/auth-extra.test-d.js.map +1 -1
- package/dist/server/express.d.ts +11 -2
- package/dist/server/express.js +22 -4
- package/dist/server/express.js.map +1 -1
- package/dist/server/express.test.js +120 -31
- package/dist/server/express.test.js.map +1 -1
- package/dist/server/file-ref.d.ts +1 -1
- package/dist/server/file-ref.js +1 -1
- package/dist/server/file-ref.js.map +1 -1
- package/dist/server/index.d.ts +1 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp-errors.test.js +5 -1
- package/dist/server/mcp-errors.test.js.map +1 -1
- package/dist/server/middleware.test.js +194 -145
- package/dist/server/middleware.test.js.map +1 -1
- package/dist/server/register-tool.test.js +9 -5
- package/dist/server/register-tool.test.js.map +1 -1
- package/dist/server/server.d.ts +29 -61
- package/dist/server/server.js +18 -66
- package/dist/server/server.js.map +1 -1
- package/dist/server/skills-integration.test.js +7 -2
- package/dist/server/skills-integration.test.js.map +1 -1
- package/dist/server/skills.js +1 -1
- package/dist/server/skills.js.map +1 -1
- package/dist/server/templateHelper.d.ts +0 -3
- package/dist/server/templateHelper.js.map +1 -1
- package/dist/server/templates.generated.js +4 -8
- package/dist/server/templates.generated.js.map +1 -1
- package/dist/server/tool-extra.test.js +32 -16
- package/dist/server/tool-extra.test.js.map +1 -1
- package/dist/server/view-resource-resolution.test.js +8 -4
- package/dist/server/view-resource-resolution.test.js.map +1 -1
- package/dist/test/utils.d.ts +3 -3
- package/dist/test/utils.js +210 -196
- package/dist/test/utils.js.map +1 -1
- package/dist/test/view.test.js +22 -17
- package/dist/test/view.test.js.map +1 -1
- package/dist/web/bridges/adaptor.test.js +1 -1
- package/dist/web/bridges/adaptor.test.js.map +1 -1
- package/dist/web/bridges/get-adaptor.test.js +2 -2
- package/dist/web/bridges/get-adaptor.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.js +1 -1
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.test.js +2 -2
- package/dist/web/bridges/mcp-app/bridge.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -1
- package/dist/web/bridges/mcp-app/view-tools.test.js +1 -1
- package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -1
- package/dist/web/bridges/types.d.ts +6 -9
- package/dist/web/bridges/types.js.map +1 -1
- package/dist/web/create-store.test.js +2 -2
- package/dist/web/create-store.test.js.map +1 -1
- package/dist/web/data-llm.test.js +2 -2
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/generate-helpers.d.ts +15 -12
- package/dist/web/generate-helpers.js +15 -12
- package/dist/web/generate-helpers.js.map +1 -1
- package/dist/web/hooks/use-call-tool.test.js +2 -2
- package/dist/web/hooks/use-call-tool.test.js.map +1 -1
- package/dist/web/hooks/use-display-mode.test.js +1 -1
- package/dist/web/hooks/use-display-mode.test.js.map +1 -1
- package/dist/web/hooks/use-download.test.js +3 -3
- package/dist/web/hooks/use-download.test.js.map +1 -1
- package/dist/web/hooks/use-files.test.js +1 -1
- package/dist/web/hooks/use-files.test.js.map +1 -1
- package/dist/web/hooks/use-host.test.js +1 -1
- package/dist/web/hooks/use-host.test.js.map +1 -1
- package/dist/web/hooks/use-open-external.test.js +2 -2
- package/dist/web/hooks/use-open-external.test.js.map +1 -1
- package/dist/web/hooks/use-request-close.test.js +1 -1
- package/dist/web/hooks/use-request-close.test.js.map +1 -1
- package/dist/web/hooks/use-request-modal.test.js +1 -1
- package/dist/web/hooks/use-request-modal.test.js.map +1 -1
- package/dist/web/hooks/use-request-size.test.js +1 -1
- package/dist/web/hooks/use-request-size.test.js.map +1 -1
- package/dist/web/hooks/use-set-open-in-app-url.test.js +2 -8
- package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test.js +2 -2
- package/dist/web/hooks/use-tool-info.test.js.map +1 -1
- package/dist/web/hooks/use-user.test.js +2 -2
- package/dist/web/hooks/use-user.test.js.map +1 -1
- package/dist/web/hooks/use-view-state.test.js +2 -2
- package/dist/web/hooks/use-view-state.test.js.map +1 -1
- package/dist/web/hooks/use-viewport.test.js +2 -2
- package/dist/web/hooks/use-viewport.test.js.map +1 -1
- package/dist/web/index.d.ts +0 -1
- package/dist/web/index.js.map +1 -1
- package/dist/web/mount-view.js +2 -6
- package/dist/web/mount-view.js.map +1 -1
- package/package.json +5 -5
- package/dist/standard-schema.d.ts +0 -3
- package/dist/standard-schema.js +0 -2
- package/dist/standard-schema.js.map +0 -1
package/dist/server/app.d.ts
CHANGED
|
@@ -1,41 +1,89 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Implementation, Server as SdkServer, ServerOptions } from "@modelcontextprotocol/server";
|
|
2
2
|
import type { ErrorRequestHandler, Express, RequestHandler } from "express";
|
|
3
|
+
import type { OAuthConfig } from "./auth/index.js";
|
|
3
4
|
import type { ExtraClaims } from "./auth.js";
|
|
4
|
-
import { McpServer, type McpServerTypes, type
|
|
5
|
+
import { type JsonOptions, McpServer, type McpServerTypes, type ToolDef } from "./server.js";
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
7
|
+
* The bare {@link McpServer} a {@link SkybridgeHandler} receives: no tools
|
|
8
|
+
* registered yet. Use it to annotate a handler extracted into its own
|
|
9
|
+
* declaration — `(server: SkybridgeServer) => server.registerTool(…)` — and
|
|
10
|
+
* pass the claims your OAuth verifier produces to type
|
|
11
|
+
* `extra.http.authInfo.extra` in handlers. Leave the handler's return type
|
|
12
|
+
* inferred: the returned chain is what carries the tool registry into
|
|
13
|
+
* `typeof app`.
|
|
10
14
|
*/
|
|
11
|
-
export type
|
|
15
|
+
export type SkybridgeServer<TAuthExtra extends ExtraClaims = ExtraClaims> = McpServer<Record<never, ToolDef>, TAuthExtra>;
|
|
12
16
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
17
|
+
* The `handler` field of {@link SkybridgeConfig}: builds the app's MCP
|
|
18
|
+
* surface. Runs again for **every incoming request**, on a fresh
|
|
19
|
+
* {@link McpServer}, so keep it to registration: hoist pools, timers, clients
|
|
20
|
+
* and any other side effect to module scope (or into `setup`) and close over
|
|
21
|
+
* them. It must **return** the chained server so `typeof app` carries the
|
|
22
|
+
* registered tool types.
|
|
16
23
|
*
|
|
17
|
-
*
|
|
18
|
-
|
|
24
|
+
* @typeParam TContext - What `setup` resolved to, passed as the second argument.
|
|
25
|
+
*/
|
|
26
|
+
export type SkybridgeHandler<TTools extends Record<string, ToolDef>, TContext, TAuthExtra extends ExtraClaims> = (server: McpServer<Record<never, ToolDef>, TAuthExtra>, context: TContext) => McpServer<TTools, TAuthExtra>;
|
|
27
|
+
/**
|
|
28
|
+
* What the `oauth` field accepts: a resolved {@link OAuthConfig}, a promise of
|
|
29
|
+
* one (the branded providers are async), or a function of the `setup` result.
|
|
30
|
+
* A function or promise is resolved once — at {@link Skybridge.run} or on the
|
|
31
|
+
* first request, never at module import — so prefer a function when building
|
|
32
|
+
* the config has side effects (network discovery, secrets).
|
|
19
33
|
*/
|
|
20
|
-
export type
|
|
34
|
+
export type SkybridgeOAuthInput<TContext, TExtra extends ExtraClaims> = OAuthConfig<TExtra> | Promise<OAuthConfig<TExtra>> | ((context: TContext) => OAuthConfig<TExtra> | Promise<OAuthConfig<TExtra>>);
|
|
35
|
+
/**
|
|
36
|
+
* Everything a Skybridge app needs in one bag: the MCP implementation info
|
|
37
|
+
* (`name`, `version`, …), the SDK's {@link ServerOptions} (`capabilities`,
|
|
38
|
+
* `instructions`, …), the Express and skills options, and the app's behavior
|
|
39
|
+
* (`setup`, `oauth`, `handler`).
|
|
40
|
+
*
|
|
41
|
+
* All type parameters are inferred from the value: the context from `setup`,
|
|
42
|
+
* the auth claims from `oauth`, and the tool registry from the server
|
|
43
|
+
* `handler` returns.
|
|
44
|
+
*/
|
|
45
|
+
export type SkybridgeConfig<TTools extends Record<string, ToolDef> = Record<never, ToolDef>, TContext = undefined, TAuthExtra extends ExtraClaims = ExtraClaims> = Implementation & ServerOptions & {
|
|
46
|
+
/** Options for the built-in `express.json()` middleware, e.g. `{ limit: "10mb" }`. */
|
|
47
|
+
json?: JsonOptions;
|
|
48
|
+
/**
|
|
49
|
+
* @experimental Serve Agent Skills from `src/skills` over MCP (SEP-2640).
|
|
50
|
+
* API may change.
|
|
51
|
+
*/
|
|
52
|
+
skills?: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Loads whatever the app needs up front (remote config, secrets, datasets,
|
|
55
|
+
* …). Runs **once** — at {@link Skybridge.run} or on the first request,
|
|
56
|
+
* never at module import — and its awaited return value is passed to an
|
|
57
|
+
* `oauth` function and to `handler` as the second argument.
|
|
58
|
+
*/
|
|
59
|
+
setup?: () => TContext;
|
|
60
|
+
/**
|
|
61
|
+
* Resource-server OAuth. When set, mounts the well-known metadata routes
|
|
62
|
+
* and bearer auth on `/mcp`, and the verifier's claims type
|
|
63
|
+
* `extra.http.authInfo.extra` in tool handlers.
|
|
64
|
+
*/
|
|
65
|
+
oauth?: SkybridgeOAuthInput<Awaited<TContext>, TAuthExtra>;
|
|
66
|
+
/** Registers the MCP surface, per request. See {@link SkybridgeHandler}. */
|
|
67
|
+
handler: SkybridgeHandler<TTools, Awaited<TContext>, TAuthExtra>;
|
|
68
|
+
};
|
|
21
69
|
/**
|
|
22
70
|
* A Skybridge app: the HTTP surface (Express, OAuth metadata, the `/mcp`
|
|
23
|
-
* route) plus a
|
|
71
|
+
* route) plus a handler that builds the MCP server for each request.
|
|
24
72
|
*
|
|
25
|
-
* The
|
|
73
|
+
* The handler runs for every request, so tools, resources, prompts and views
|
|
26
74
|
* are always registered on the instance that serves the request. Anything in
|
|
27
|
-
* the
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* @typeParam TTools - Accumulated tool registry, inferred from the server the
|
|
32
|
-
* factory returns. You almost never set this manually.
|
|
75
|
+
* the handler body other than registration therefore runs per request too.
|
|
76
|
+
* Anything asynchronous the app needs (remote config, secrets, …) goes in
|
|
77
|
+
* `setup`, which runs once and feeds the handler's second argument.
|
|
33
78
|
*
|
|
34
79
|
* @example
|
|
35
80
|
* ```ts
|
|
36
|
-
* export const app = new Skybridge(
|
|
37
|
-
*
|
|
38
|
-
*
|
|
81
|
+
* export const app = new Skybridge({
|
|
82
|
+
* name: "my-app",
|
|
83
|
+
* version: "1.0.0",
|
|
84
|
+
* setup: async () => loadConfig(),
|
|
85
|
+
* oauth: (config) => descopeProvider({ url: config.mcpServerUrl }),
|
|
86
|
+
* handler: (server, config) =>
|
|
39
87
|
* server.registerTool(
|
|
40
88
|
* {
|
|
41
89
|
* name: "search",
|
|
@@ -44,32 +92,45 @@ export type SkybridgeFactory<TTools extends Record<string, ToolDef>, TAuthExtra
|
|
|
44
92
|
* },
|
|
45
93
|
* async ({ query }) => ({ content: `Results for ${query}` }),
|
|
46
94
|
* ),
|
|
47
|
-
* );
|
|
95
|
+
* });
|
|
48
96
|
*
|
|
49
97
|
* export type AppType = typeof app;
|
|
50
98
|
* ```
|
|
51
99
|
*
|
|
52
100
|
* @see https://docs.skybridge.tech/api-reference/mcp-server
|
|
53
101
|
*/
|
|
54
|
-
export declare class Skybridge<TTools extends Record<string, ToolDef> = Record<never, ToolDef>, TAuthExtra extends ExtraClaims = ExtraClaims> {
|
|
102
|
+
export declare class Skybridge<TTools extends Record<string, ToolDef> = Record<never, ToolDef>, TContext = undefined, TAuthExtra extends ExtraClaims = ExtraClaims> {
|
|
55
103
|
readonly $types: McpServerTypes<TTools>;
|
|
56
104
|
private readonly serverInfo;
|
|
57
105
|
private readonly serverOptions;
|
|
58
|
-
private readonly
|
|
59
|
-
private
|
|
106
|
+
private readonly skills?;
|
|
107
|
+
private oauthEnabled;
|
|
108
|
+
private readonly handler;
|
|
109
|
+
private readonly setup?;
|
|
110
|
+
private readonly oauthInput?;
|
|
111
|
+
private context?;
|
|
60
112
|
private readonly expressApp;
|
|
61
113
|
private readonly errorMiddleware;
|
|
62
114
|
private readonly monitoringEntry;
|
|
63
115
|
private resolveResourceMetadataUrl?;
|
|
64
|
-
private
|
|
65
|
-
|
|
116
|
+
private readyPromise?;
|
|
117
|
+
private slowHandlerWarned;
|
|
118
|
+
constructor(config: SkybridgeConfig<TTools, TContext, TAuthExtra>);
|
|
119
|
+
/**
|
|
120
|
+
* Resolve `setup` and `oauth`, then wire OAuth onto the Express app. Runs
|
|
121
|
+
* once; every entry point that needs a built server awaits it, and a failed
|
|
122
|
+
* attempt is retried on the next call.
|
|
123
|
+
*
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
ready(): Promise<void>;
|
|
66
127
|
/**
|
|
67
128
|
* The underlying Express app. Use this to extend the HTTP server with
|
|
68
129
|
* custom routes, middleware, or settings — e.g.
|
|
69
130
|
* `app.express.get("/health", ...)`.
|
|
70
131
|
*
|
|
71
132
|
* `express.json()` is pre-applied — tune it via the `json` config field,
|
|
72
|
-
* e.g. `new Skybridge({ name, version, json: { limit: "10mb" } }
|
|
133
|
+
* e.g. `new Skybridge({ name, version, json: { limit: "10mb" }, handler })`.
|
|
73
134
|
* Register your handlers before `run()`; after `run()`, dev-mode middleware,
|
|
74
135
|
* the `/mcp` route, and the default error handler are appended in that order.
|
|
75
136
|
*
|
|
@@ -77,21 +138,17 @@ export declare class Skybridge<TTools extends Record<string, ToolDef> = Record<n
|
|
|
77
138
|
* locally and on self-hosted deployments.
|
|
78
139
|
*/
|
|
79
140
|
get express(): Express;
|
|
80
|
-
/**
|
|
81
|
-
* The app's fetch handler for `/mcp`: a `Request` → `Response` function that
|
|
82
|
-
* builds a fresh MCP server per request. Memoized, so the same handler is
|
|
83
|
-
* reused across requests.
|
|
84
|
-
*/
|
|
85
|
-
get fetchHandler(): McpHttpHandler;
|
|
86
141
|
/**
|
|
87
142
|
* Build a fresh server for one stateless HTTP request, as
|
|
88
|
-
* `createMcpHandler`'s factory contract requires: the
|
|
143
|
+
* `createMcpHandler`'s factory contract requires: the handler runs again so
|
|
89
144
|
* the SDK's handler closures belong to the instance whose protocol era it
|
|
90
145
|
* stamps. Sharing one instance's handler maps instead would bind them to an
|
|
91
146
|
* instance that is never marked, pinning every request to the 2025 codec and
|
|
92
147
|
* letting concurrent callers overwrite each other's negotiated version.
|
|
148
|
+
*
|
|
149
|
+
* Awaits `setup` and the `oauth` input on first use.
|
|
93
150
|
*/
|
|
94
|
-
createServerInstance(): SdkServer
|
|
151
|
+
createServerInstance(): Promise<SdkServer>;
|
|
95
152
|
/**
|
|
96
153
|
* Connect a Skybridge app to an MCP transport. Use this when you're
|
|
97
154
|
* embedding Skybridge in a host that already manages its own transport
|
package/dist/server/app.js
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
-
import { createMcpHandler, UnsupportedProtocolVersionError, } from "@modelcontextprotocol/server";
|
|
3
2
|
import { setupOAuth } from "./auth/setup.js";
|
|
4
|
-
import { createApp, createBaseApp } from "./express.js";
|
|
3
|
+
import { buildMcpHandler, createApp, createBaseApp } from "./express.js";
|
|
5
4
|
import { createMiddlewareEntry } from "./metric.js";
|
|
6
5
|
import { buildMiddlewareChain, getHandlerMaps } from "./middleware.js";
|
|
7
6
|
import { McpServer, } from "./server.js";
|
|
7
|
+
const SLOW_HANDLER_THRESHOLD_MS = 50;
|
|
8
8
|
/**
|
|
9
9
|
* A Skybridge app: the HTTP surface (Express, OAuth metadata, the `/mcp`
|
|
10
|
-
* route) plus a
|
|
10
|
+
* route) plus a handler that builds the MCP server for each request.
|
|
11
11
|
*
|
|
12
|
-
* The
|
|
12
|
+
* The handler runs for every request, so tools, resources, prompts and views
|
|
13
13
|
* are always registered on the instance that serves the request. Anything in
|
|
14
|
-
* the
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* @typeParam TTools - Accumulated tool registry, inferred from the server the
|
|
19
|
-
* factory returns. You almost never set this manually.
|
|
14
|
+
* the handler body other than registration therefore runs per request too.
|
|
15
|
+
* Anything asynchronous the app needs (remote config, secrets, …) goes in
|
|
16
|
+
* `setup`, which runs once and feeds the handler's second argument.
|
|
20
17
|
*
|
|
21
18
|
* @example
|
|
22
19
|
* ```ts
|
|
23
|
-
* export const app = new Skybridge(
|
|
24
|
-
*
|
|
25
|
-
*
|
|
20
|
+
* export const app = new Skybridge({
|
|
21
|
+
* name: "my-app",
|
|
22
|
+
* version: "1.0.0",
|
|
23
|
+
* setup: async () => loadConfig(),
|
|
24
|
+
* oauth: (config) => descopeProvider({ url: config.mcpServerUrl }),
|
|
25
|
+
* handler: (server, config) =>
|
|
26
26
|
* server.registerTool(
|
|
27
27
|
* {
|
|
28
28
|
* name: "search",
|
|
@@ -31,7 +31,7 @@ import { McpServer, } from "./server.js";
|
|
|
31
31
|
* },
|
|
32
32
|
* async ({ query }) => ({ content: `Results for ${query}` }),
|
|
33
33
|
* ),
|
|
34
|
-
* );
|
|
34
|
+
* });
|
|
35
35
|
*
|
|
36
36
|
* export type AppType = typeof app;
|
|
37
37
|
* ```
|
|
@@ -41,24 +41,53 @@ import { McpServer, } from "./server.js";
|
|
|
41
41
|
export class Skybridge {
|
|
42
42
|
serverInfo;
|
|
43
43
|
serverOptions;
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
skills;
|
|
45
|
+
oauthEnabled = false;
|
|
46
|
+
handler;
|
|
47
|
+
setup;
|
|
48
|
+
oauthInput;
|
|
49
|
+
context;
|
|
46
50
|
expressApp;
|
|
47
51
|
errorMiddleware = [];
|
|
48
52
|
monitoringEntry = createMiddlewareEntry();
|
|
49
53
|
resolveResourceMetadataUrl;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
readyPromise;
|
|
55
|
+
slowHandlerWarned = false;
|
|
56
|
+
constructor(config) {
|
|
57
|
+
const { name, title, version, description, icons, websiteUrl, json, skills, setup, oauth, handler, ...serverOptions } = config;
|
|
53
58
|
this.serverInfo = { name, title, version, description, icons, websiteUrl };
|
|
54
59
|
this.serverOptions = serverOptions;
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
-
|
|
60
|
+
this.skills = skills;
|
|
61
|
+
this.setup = setup;
|
|
62
|
+
this.oauthInput = oauth;
|
|
63
|
+
this.handler = handler;
|
|
58
64
|
this.expressApp = createBaseApp(json);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Resolve `setup` and `oauth`, then wire OAuth onto the Express app. Runs
|
|
68
|
+
* once; every entry point that needs a built server awaits it, and a failed
|
|
69
|
+
* attempt is retried on the next call.
|
|
70
|
+
*
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
ready() {
|
|
74
|
+
this.readyPromise ??= (async () => {
|
|
75
|
+
if (this.setup) {
|
|
76
|
+
this.context = await this.setup();
|
|
77
|
+
}
|
|
78
|
+
const oauth = typeof this.oauthInput === "function"
|
|
79
|
+
? await this.oauthInput(this.context)
|
|
80
|
+
: await this.oauthInput;
|
|
81
|
+
this.oauthEnabled = Boolean(oauth);
|
|
82
|
+
const sample = this.buildServer();
|
|
83
|
+
if (oauth) {
|
|
84
|
+
this.resolveResourceMetadataUrl = setupOAuth(this.expressApp, oauth, sample.securitySchemesByTool);
|
|
85
|
+
}
|
|
86
|
+
})().catch((error) => {
|
|
87
|
+
this.readyPromise = undefined;
|
|
88
|
+
throw error;
|
|
89
|
+
});
|
|
90
|
+
return this.readyPromise;
|
|
62
91
|
}
|
|
63
92
|
/**
|
|
64
93
|
* The underlying Express app. Use this to extend the HTTP server with
|
|
@@ -66,7 +95,7 @@ export class Skybridge {
|
|
|
66
95
|
* `app.express.get("/health", ...)`.
|
|
67
96
|
*
|
|
68
97
|
* `express.json()` is pre-applied — tune it via the `json` config field,
|
|
69
|
-
* e.g. `new Skybridge({ name, version, json: { limit: "10mb" } }
|
|
98
|
+
* e.g. `new Skybridge({ name, version, json: { limit: "10mb" }, handler })`.
|
|
70
99
|
* Register your handlers before `run()`; after `run()`, dev-mode middleware,
|
|
71
100
|
* the `/mcp` route, and the default error handler are appended in that order.
|
|
72
101
|
*
|
|
@@ -76,31 +105,18 @@ export class Skybridge {
|
|
|
76
105
|
get express() {
|
|
77
106
|
return this.expressApp;
|
|
78
107
|
}
|
|
79
|
-
/**
|
|
80
|
-
* The app's fetch handler for `/mcp`: a `Request` → `Response` function that
|
|
81
|
-
* builds a fresh MCP server per request. Memoized, so the same handler is
|
|
82
|
-
* reused across requests.
|
|
83
|
-
*/
|
|
84
|
-
get fetchHandler() {
|
|
85
|
-
this.cachedFetchHandler ??= createMcpHandler(() => this.createServerInstance(), {
|
|
86
|
-
onerror: (error) => {
|
|
87
|
-
if (error instanceof UnsupportedProtocolVersionError) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
console.error("Error handling MCP request:", error);
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
return this.cachedFetchHandler;
|
|
94
|
-
}
|
|
95
108
|
/**
|
|
96
109
|
* Build a fresh server for one stateless HTTP request, as
|
|
97
|
-
* `createMcpHandler`'s factory contract requires: the
|
|
110
|
+
* `createMcpHandler`'s factory contract requires: the handler runs again so
|
|
98
111
|
* the SDK's handler closures belong to the instance whose protocol era it
|
|
99
112
|
* stamps. Sharing one instance's handler maps instead would bind them to an
|
|
100
113
|
* instance that is never marked, pinning every request to the 2025 codec and
|
|
101
114
|
* letting concurrent callers overwrite each other's negotiated version.
|
|
115
|
+
*
|
|
116
|
+
* Awaits `setup` and the `oauth` input on first use.
|
|
102
117
|
*/
|
|
103
|
-
createServerInstance() {
|
|
118
|
+
async createServerInstance() {
|
|
119
|
+
await this.ready();
|
|
104
120
|
const server = this.buildServer();
|
|
105
121
|
this.instrumentHandlers(server);
|
|
106
122
|
return server.server;
|
|
@@ -112,7 +128,8 @@ export class Skybridge {
|
|
|
112
128
|
* which sets the transport up for you.
|
|
113
129
|
*/
|
|
114
130
|
async connect(transport) {
|
|
115
|
-
await this.createServerInstance()
|
|
131
|
+
const instance = await this.createServerInstance();
|
|
132
|
+
await instance.connect(transport);
|
|
116
133
|
}
|
|
117
134
|
use(pathOrHandler, ...handlers) {
|
|
118
135
|
// Branching is load-bearing: Express's `app.use` overloads can't be
|
|
@@ -162,9 +179,11 @@ export class Skybridge {
|
|
|
162
179
|
return this.expressApp;
|
|
163
180
|
}
|
|
164
181
|
const httpServer = http.createServer();
|
|
182
|
+
const mcpHandler = buildMcpHandler(this);
|
|
165
183
|
await createApp({
|
|
166
184
|
app: this,
|
|
167
185
|
httpServer,
|
|
186
|
+
mcpHandler,
|
|
168
187
|
errorMiddleware: this.errorMiddleware,
|
|
169
188
|
});
|
|
170
189
|
httpServer.on("request", this.expressApp);
|
|
@@ -194,7 +213,7 @@ export class Skybridge {
|
|
|
194
213
|
// (force-quit on a second Ctrl+C while drain is hanging).
|
|
195
214
|
process.off("SIGTERM", shutdown);
|
|
196
215
|
process.off("SIGINT", shutdown);
|
|
197
|
-
|
|
216
|
+
mcpHandler.close().catch(() => { });
|
|
198
217
|
httpServer.close(() => process.exit(0));
|
|
199
218
|
// Force exit if connections don't drain in time so the port is still
|
|
200
219
|
// released promptly (e.g. for nodemon restarts).
|
|
@@ -205,11 +224,21 @@ export class Skybridge {
|
|
|
205
224
|
return undefined;
|
|
206
225
|
}
|
|
207
226
|
buildServer() {
|
|
208
|
-
const server = new McpServer(this.serverInfo, this.serverOptions, this.
|
|
227
|
+
const server = new McpServer(this.serverInfo, this.serverOptions, { skills: this.skills, oauth: this.oauthEnabled });
|
|
209
228
|
if (this.resolveResourceMetadataUrl) {
|
|
210
229
|
server.setResourceMetadataUrlResolver(this.resolveResourceMetadataUrl);
|
|
211
230
|
}
|
|
212
|
-
|
|
231
|
+
const startedAt = performance.now();
|
|
232
|
+
const built = this.handler(server, this.context);
|
|
233
|
+
const elapsed = performance.now() - startedAt;
|
|
234
|
+
if (typeof built.then === "function") {
|
|
235
|
+
throw new Error("The Skybridge handler must be synchronous — it runs on every request. Load config or secrets in `setup` instead and read them from the handler's second argument.");
|
|
236
|
+
}
|
|
237
|
+
if (elapsed > SLOW_HANDLER_THRESHOLD_MS && !this.slowHandlerWarned) {
|
|
238
|
+
this.slowHandlerWarned = true;
|
|
239
|
+
console.warn(`The Skybridge handler took ${Math.round(elapsed)}ms — it runs on every request, so this cost is paid per request. Hoist expensive work to module scope or into \`setup\`, whose result is passed to the handler.`);
|
|
240
|
+
}
|
|
241
|
+
return built;
|
|
213
242
|
}
|
|
214
243
|
middlewareEntries(server) {
|
|
215
244
|
return [
|
package/dist/server/app.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/server/app.ts"],"names":[],"mappings":"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","sourcesContent":["import http from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\nimport {\n createMcpHandler,\n type Implementation,\n type McpHttpHandler,\n type Server as SdkServer,\n type ServerOptions,\n UnsupportedProtocolVersionError,\n} from \"@modelcontextprotocol/server\";\nimport type { ErrorRequestHandler, Express, RequestHandler } from \"express\";\nimport { type ResourceMetadataUrlResolver, setupOAuth } from \"./auth/setup.js\";\nimport type { ExtraClaims } from \"./auth.js\";\nimport { createApp, createBaseApp } from \"./express.js\";\nimport { createMiddlewareEntry } from \"./metric.js\";\nimport type { McpMiddlewareEntry } from \"./middleware.js\";\nimport { buildMiddlewareChain, getHandlerMaps } from \"./middleware.js\";\nimport {\n McpServer,\n type McpServerTypes,\n type SkybridgeServerOptions,\n type ToolDef,\n} from \"./server.js\";\n\ntype ErrorMiddlewareConfig = {\n path?: string;\n handlers: ErrorRequestHandler[];\n};\n\n/**\n * Everything a Skybridge app needs in one bag: the MCP implementation info\n * (`name`, `version`, …), the SDK's {@link ServerOptions} (`capabilities`,\n * `instructions`, …), and the Skybridge-specific options (`oauth`, `json`,\n * `skills`).\n */\nexport type SkybridgeConfig<TAuthExtra extends ExtraClaims = ExtraClaims> =\n Implementation & ServerOptions & SkybridgeServerOptions<TAuthExtra>;\n\n/**\n * Builds an app's MCP surface. Runs again for **every incoming request**, on a\n * fresh {@link McpServer}, so keep it to registration: hoist pools, timers,\n * clients and any other side effect to module scope and close over them.\n *\n * It must **return** the chained server so `typeof app` carries the registered\n * tool types.\n */\nexport type SkybridgeFactory<\n TTools extends Record<string, ToolDef>,\n TAuthExtra extends ExtraClaims,\n> = (\n server: McpServer<Record<never, ToolDef>, TAuthExtra>,\n) => McpServer<TTools, TAuthExtra>;\n\n/**\n * A Skybridge app: the HTTP surface (Express, OAuth metadata, the `/mcp`\n * route) plus a factory that builds the MCP server for each request.\n *\n * The factory runs for every request, so tools, resources, prompts and views\n * are always registered on the instance that serves the request. Anything in\n * the factory body other than registration therefore runs per request too.\n * It also runs once at construction, which surfaces registration errors at\n * boot and gives the OAuth layer the set of per-tool security schemes.\n *\n * @typeParam TTools - Accumulated tool registry, inferred from the server the\n * factory returns. You almost never set this manually.\n *\n * @example\n * ```ts\n * export const app = new Skybridge(\n * { name: \"my-app\", version: \"1.0.0\", capabilities: {} },\n * (server) =>\n * server.registerTool(\n * {\n * name: \"search\",\n * inputSchema: { query: z.string() },\n * view: { component: \"search\" },\n * },\n * async ({ query }) => ({ content: `Results for ${query}` }),\n * ),\n * );\n *\n * export type AppType = typeof app;\n * ```\n *\n * @see https://docs.skybridge.tech/api-reference/mcp-server\n */\nexport class Skybridge<\n TTools extends Record<string, ToolDef> = Record<never, ToolDef>,\n TAuthExtra extends ExtraClaims = ExtraClaims,\n> {\n declare readonly $types: McpServerTypes<TTools>;\n private readonly serverInfo: Implementation;\n private readonly serverOptions: ServerOptions;\n private readonly skybridgeOptions: SkybridgeServerOptions<TAuthExtra>;\n private readonly factory: SkybridgeFactory<TTools, TAuthExtra>;\n private readonly expressApp: Express;\n private readonly errorMiddleware: ErrorMiddlewareConfig[] = [];\n private readonly monitoringEntry: McpMiddlewareEntry | null =\n createMiddlewareEntry();\n private resolveResourceMetadataUrl?: ResourceMetadataUrlResolver;\n private cachedFetchHandler?: McpHttpHandler;\n\n constructor(\n config: SkybridgeConfig<TAuthExtra>,\n factory: SkybridgeFactory<TTools, TAuthExtra>,\n ) {\n const {\n name,\n title,\n version,\n description,\n icons,\n websiteUrl,\n json,\n oauth,\n skills,\n ...serverOptions\n } = config;\n\n this.serverInfo = { name, title, version, description, icons, websiteUrl };\n this.serverOptions = serverOptions;\n this.skybridgeOptions = { json, oauth, skills };\n this.factory = factory;\n\n const sample = this.buildServer();\n\n this.expressApp = createBaseApp(json);\n if (oauth) {\n this.resolveResourceMetadataUrl = setupOAuth(\n this.expressApp,\n oauth,\n sample.securitySchemesByTool,\n );\n }\n }\n\n /**\n * The underlying Express app. Use this to extend the HTTP server with\n * custom routes, middleware, or settings — e.g.\n * `app.express.get(\"/health\", ...)`.\n *\n * `express.json()` is pre-applied — tune it via the `json` config field,\n * e.g. `new Skybridge({ name, version, json: { limit: \"10mb\" } }, setup)`.\n * Register your handlers before `run()`; after `run()`, dev-mode middleware,\n * the `/mcp` route, and the default error handler are appended in that order.\n *\n * Note: Alpic Cloud only routes traffic to `/mcp` — custom routes work\n * locally and on self-hosted deployments.\n */\n get express(): Express {\n return this.expressApp;\n }\n\n /**\n * The app's fetch handler for `/mcp`: a `Request` → `Response` function that\n * builds a fresh MCP server per request. Memoized, so the same handler is\n * reused across requests.\n */\n get fetchHandler(): McpHttpHandler {\n this.cachedFetchHandler ??= createMcpHandler(\n () => this.createServerInstance(),\n {\n onerror: (error) => {\n if (error instanceof UnsupportedProtocolVersionError) {\n return;\n }\n console.error(\"Error handling MCP request:\", error);\n },\n },\n );\n return this.cachedFetchHandler;\n }\n\n /**\n * Build a fresh server for one stateless HTTP request, as\n * `createMcpHandler`'s factory contract requires: the factory runs again so\n * the SDK's handler closures belong to the instance whose protocol era it\n * stamps. Sharing one instance's handler maps instead would bind them to an\n * instance that is never marked, pinning every request to the 2025 codec and\n * letting concurrent callers overwrite each other's negotiated version.\n */\n createServerInstance(): SdkServer {\n const server = this.buildServer();\n this.instrumentHandlers(server);\n return server.server;\n }\n\n /**\n * Connect a Skybridge app to an MCP transport. Use this when you're\n * embedding Skybridge in a host that already manages its own transport\n * (e.g. stdio for desktop apps); for HTTP, prefer {@link Skybridge.run}\n * which sets the transport up for you.\n */\n async connect(transport: Parameters<SdkServer[\"connect\"]>[0]): Promise<void> {\n await this.createServerInstance().connect(transport);\n }\n\n /**\n * Register Express middleware on the underlying app. Mirrors `app.use` —\n * pass handlers directly or a path-prefixed handler list. Register before\n * {@link Skybridge.run}; ordering matches Express.\n *\n * Note: Alpic Cloud only routes traffic to `/mcp`. Custom paths work\n * locally and on self-hosted deployments.\n */\n use(...handlers: RequestHandler[]): this;\n use(path: string, ...handlers: RequestHandler[]): this;\n use(\n pathOrHandler: string | RequestHandler,\n ...handlers: RequestHandler[]\n ): this {\n // Branching is load-bearing: Express's `app.use` overloads can't be\n // resolved against a `string | RequestHandler` union, so we narrow.\n if (typeof pathOrHandler === \"string\") {\n this.expressApp.use(pathOrHandler, ...handlers);\n } else {\n this.expressApp.use(pathOrHandler, ...handlers);\n }\n return this;\n }\n\n /**\n * Register Express error-handling middleware to run after the built-in\n * `/mcp` route (or your custom route). Use this to log or transform errors\n * thrown by tool handlers before the default error handler responds.\n *\n * @example\n * ```ts\n * app.useOnError((err, _req, _res, next) => {\n * logger.error(err);\n * next(err);\n * });\n * ```\n */\n useOnError(...handlers: ErrorRequestHandler[]): this;\n useOnError(path: string, ...handlers: ErrorRequestHandler[]): this;\n useOnError(\n pathOrHandler: string | ErrorRequestHandler,\n ...handlers: ErrorRequestHandler[]\n ): this {\n if (typeof pathOrHandler === \"string\") {\n this.errorMiddleware.push({ path: pathOrHandler, handlers });\n } else {\n this.errorMiddleware.push({ handlers: [pathOrHandler, ...handlers] });\n }\n return this;\n }\n\n /**\n * Start the HTTP server. Listens on `process.env.__PORT` (default `3000`),\n * mounts the `/mcp` route, and applies any custom Express middleware\n * registered via {@link Skybridge.use} / {@link Skybridge.useOnError}.\n *\n * On Cloudflare Workers / workerd, returns an object exposing `fetch` so\n * the runtime can bridge incoming requests to the Node HTTP server. On\n * Vercel (`VERCEL === \"1\"`), returns the Express app directly so the\n * serverless function entry can call it as a `(req, res)` handler. On\n * Node, returns `undefined` once listening. When the process was spawned\n * with an IPC channel, the bound port is reported to the parent as\n * `{ type: \"skybridge:listening\", port }`, the readiness signal test runners\n * wait on instead of polling.\n */\n async run(): Promise<\n { fetch: (...args: unknown[]) => unknown } | Express | undefined\n > {\n if (process.env.VERCEL === \"1\") {\n // createApp only reads httpServer inside its dev-only branch\n // (viewsDevServer); under VERCEL=1 + NODE_ENV=production it's a\n // bare object passed to satisfy the required parameter.\n const httpServer = http.createServer();\n await createApp({\n app: this,\n httpServer,\n errorMiddleware: this.errorMiddleware,\n });\n return this.expressApp;\n }\n\n const httpServer = http.createServer();\n\n await createApp({\n app: this,\n httpServer,\n errorMiddleware: this.errorMiddleware,\n });\n\n httpServer.on(\"request\", this.expressApp);\n const intendedPort = parseInt(process.env.__PORT ?? \"3000\", 10);\n await new Promise<void>((resolve, reject) => {\n httpServer.on(\"error\", (error: Error) => {\n console.error(\"Failed to start server:\", error);\n reject(error);\n });\n httpServer.listen(intendedPort, () => {\n resolve();\n });\n });\n\n const { port } = httpServer.address() as AddressInfo;\n process.send?.({ type: \"skybridge:listening\", port });\n\n // On workerd, bridge the Node http server to a Workers fetch handler.\n // The specifier is held in a variable to sidestep tsc's module resolution\n // (`cloudflare:node` only exists under wrangler/workerd).\n if (\n typeof navigator !== \"undefined\" &&\n navigator.userAgent === \"Cloudflare-Workers\"\n ) {\n const cloudflareNode = \"cloudflare:node\";\n const { httpServerHandler } = await import(cloudflareNode);\n return httpServerHandler({ port });\n }\n\n const shutdown = () => {\n // Drop both handlers so a second signal falls through to Node's default\n // (force-quit on a second Ctrl+C while drain is hanging).\n process.off(\"SIGTERM\", shutdown);\n process.off(\"SIGINT\", shutdown);\n this.cachedFetchHandler?.close().catch(() => {});\n httpServer.close(() => process.exit(0));\n // Force exit if connections don't drain in time so the port is still\n // released promptly (e.g. for nodemon restarts).\n setTimeout(() => process.exit(0), 3000).unref();\n };\n process.on(\"SIGTERM\", shutdown);\n process.on(\"SIGINT\", shutdown);\n return undefined;\n }\n\n private buildServer(): McpServer<TTools, TAuthExtra> {\n const server = new McpServer<Record<never, ToolDef>, TAuthExtra>(\n this.serverInfo,\n this.serverOptions,\n this.skybridgeOptions,\n );\n if (this.resolveResourceMetadataUrl) {\n server.setResourceMetadataUrlResolver(this.resolveResourceMetadataUrl);\n }\n return this.factory(server);\n }\n\n private middlewareEntries(\n server: McpServer<Record<never, ToolDef>, TAuthExtra>,\n ): McpMiddlewareEntry[] {\n return [\n ...(this.monitoringEntry ? [this.monitoringEntry] : []),\n ...server.protocolMiddlewareEntries(),\n ];\n }\n\n private instrumentHandlers(\n server: McpServer<Record<never, ToolDef>, TAuthExtra>,\n ): void {\n const entries = this.middlewareEntries(server);\n const { requestHandlers, notificationHandlers } = getHandlerMaps(\n server.server,\n );\n\n const instrumentMap = (\n map: Map<string, (...args: unknown[]) => Promise<unknown>>,\n isNotification: boolean,\n ) => {\n for (const [method, handler] of map) {\n map.set(\n method,\n buildMiddlewareChain(method, isNotification, handler, entries),\n );\n }\n const originalSet = map.set.bind(map);\n map.set = (\n method: string,\n handler: (...args: unknown[]) => Promise<unknown>,\n ) =>\n originalSet(\n method,\n buildMiddlewareChain(method, isNotification, handler, entries),\n );\n };\n\n instrumentMap(requestHandlers, false);\n instrumentMap(notificationHandlers, true);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/server/app.ts"],"names":[],"mappings":"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","sourcesContent":["import http from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\nimport type {\n Implementation,\n Server as SdkServer,\n ServerOptions,\n} from \"@modelcontextprotocol/server\";\nimport type { ErrorRequestHandler, Express, RequestHandler } from \"express\";\nimport type { OAuthConfig } from \"./auth/index.js\";\nimport { type ResourceMetadataUrlResolver, setupOAuth } from \"./auth/setup.js\";\nimport type { ExtraClaims } from \"./auth.js\";\nimport { buildMcpHandler, createApp, createBaseApp } from \"./express.js\";\nimport { createMiddlewareEntry } from \"./metric.js\";\nimport type { McpMiddlewareEntry } from \"./middleware.js\";\nimport { buildMiddlewareChain, getHandlerMaps } from \"./middleware.js\";\nimport {\n type JsonOptions,\n McpServer,\n type McpServerTypes,\n type ToolDef,\n} from \"./server.js\";\n\nconst SLOW_HANDLER_THRESHOLD_MS = 50;\n\ntype ErrorMiddlewareConfig = {\n path?: string;\n handlers: ErrorRequestHandler[];\n};\n\n/**\n * The bare {@link McpServer} a {@link SkybridgeHandler} receives: no tools\n * registered yet. Use it to annotate a handler extracted into its own\n * declaration — `(server: SkybridgeServer) => server.registerTool(…)` — and\n * pass the claims your OAuth verifier produces to type\n * `extra.http.authInfo.extra` in handlers. Leave the handler's return type\n * inferred: the returned chain is what carries the tool registry into\n * `typeof app`.\n */\nexport type SkybridgeServer<TAuthExtra extends ExtraClaims = ExtraClaims> =\n McpServer<Record<never, ToolDef>, TAuthExtra>;\n\n/**\n * The `handler` field of {@link SkybridgeConfig}: builds the app's MCP\n * surface. Runs again for **every incoming request**, on a fresh\n * {@link McpServer}, so keep it to registration: hoist pools, timers, clients\n * and any other side effect to module scope (or into `setup`) and close over\n * them. It must **return** the chained server so `typeof app` carries the\n * registered tool types.\n *\n * @typeParam TContext - What `setup` resolved to, passed as the second argument.\n */\nexport type SkybridgeHandler<\n TTools extends Record<string, ToolDef>,\n TContext,\n TAuthExtra extends ExtraClaims,\n> = (\n server: McpServer<Record<never, ToolDef>, TAuthExtra>,\n context: TContext,\n) => McpServer<TTools, TAuthExtra>;\n\n/**\n * What the `oauth` field accepts: a resolved {@link OAuthConfig}, a promise of\n * one (the branded providers are async), or a function of the `setup` result.\n * A function or promise is resolved once — at {@link Skybridge.run} or on the\n * first request, never at module import — so prefer a function when building\n * the config has side effects (network discovery, secrets).\n */\nexport type SkybridgeOAuthInput<TContext, TExtra extends ExtraClaims> =\n | OAuthConfig<TExtra>\n | Promise<OAuthConfig<TExtra>>\n | ((context: TContext) => OAuthConfig<TExtra> | Promise<OAuthConfig<TExtra>>);\n\n/**\n * Everything a Skybridge app needs in one bag: the MCP implementation info\n * (`name`, `version`, …), the SDK's {@link ServerOptions} (`capabilities`,\n * `instructions`, …), the Express and skills options, and the app's behavior\n * (`setup`, `oauth`, `handler`).\n *\n * All type parameters are inferred from the value: the context from `setup`,\n * the auth claims from `oauth`, and the tool registry from the server\n * `handler` returns.\n */\nexport type SkybridgeConfig<\n TTools extends Record<string, ToolDef> = Record<never, ToolDef>,\n TContext = undefined,\n TAuthExtra extends ExtraClaims = ExtraClaims,\n> = Implementation &\n ServerOptions & {\n /** Options for the built-in `express.json()` middleware, e.g. `{ limit: \"10mb\" }`. */\n json?: JsonOptions;\n /**\n * @experimental Serve Agent Skills from `src/skills` over MCP (SEP-2640).\n * API may change.\n */\n skills?: boolean;\n /**\n * Loads whatever the app needs up front (remote config, secrets, datasets,\n * …). Runs **once** — at {@link Skybridge.run} or on the first request,\n * never at module import — and its awaited return value is passed to an\n * `oauth` function and to `handler` as the second argument.\n */\n setup?: () => TContext;\n /**\n * Resource-server OAuth. When set, mounts the well-known metadata routes\n * and bearer auth on `/mcp`, and the verifier's claims type\n * `extra.http.authInfo.extra` in tool handlers.\n */\n oauth?: SkybridgeOAuthInput<Awaited<TContext>, TAuthExtra>;\n /** Registers the MCP surface, per request. See {@link SkybridgeHandler}. */\n handler: SkybridgeHandler<TTools, Awaited<TContext>, TAuthExtra>;\n };\n\n/**\n * A Skybridge app: the HTTP surface (Express, OAuth metadata, the `/mcp`\n * route) plus a handler that builds the MCP server for each request.\n *\n * The handler runs for every request, so tools, resources, prompts and views\n * are always registered on the instance that serves the request. Anything in\n * the handler body other than registration therefore runs per request too.\n * Anything asynchronous the app needs (remote config, secrets, …) goes in\n * `setup`, which runs once and feeds the handler's second argument.\n *\n * @example\n * ```ts\n * export const app = new Skybridge({\n * name: \"my-app\",\n * version: \"1.0.0\",\n * setup: async () => loadConfig(),\n * oauth: (config) => descopeProvider({ url: config.mcpServerUrl }),\n * handler: (server, config) =>\n * server.registerTool(\n * {\n * name: \"search\",\n * inputSchema: { query: z.string() },\n * view: { component: \"search\" },\n * },\n * async ({ query }) => ({ content: `Results for ${query}` }),\n * ),\n * });\n *\n * export type AppType = typeof app;\n * ```\n *\n * @see https://docs.skybridge.tech/api-reference/mcp-server\n */\nexport class Skybridge<\n TTools extends Record<string, ToolDef> = Record<never, ToolDef>,\n TContext = undefined,\n TAuthExtra extends ExtraClaims = ExtraClaims,\n> {\n declare readonly $types: McpServerTypes<TTools>;\n private readonly serverInfo: Implementation;\n private readonly serverOptions: ServerOptions;\n private readonly skills?: boolean;\n private oauthEnabled = false;\n private readonly handler: SkybridgeHandler<\n TTools,\n Awaited<TContext>,\n TAuthExtra\n >;\n private readonly setup?: () => TContext;\n private readonly oauthInput?: SkybridgeOAuthInput<\n Awaited<TContext>,\n TAuthExtra\n >;\n private context?: Awaited<TContext>;\n private readonly expressApp: Express;\n private readonly errorMiddleware: ErrorMiddlewareConfig[] = [];\n private readonly monitoringEntry: McpMiddlewareEntry | null =\n createMiddlewareEntry();\n private resolveResourceMetadataUrl?: ResourceMetadataUrlResolver;\n private readyPromise?: Promise<void>;\n private slowHandlerWarned = false;\n\n constructor(config: SkybridgeConfig<TTools, TContext, TAuthExtra>) {\n const {\n name,\n title,\n version,\n description,\n icons,\n websiteUrl,\n json,\n skills,\n setup,\n oauth,\n handler,\n ...serverOptions\n } = config;\n\n this.serverInfo = { name, title, version, description, icons, websiteUrl };\n this.serverOptions = serverOptions;\n this.skills = skills;\n this.setup = setup;\n this.oauthInput = oauth;\n this.handler = handler;\n this.expressApp = createBaseApp(json);\n }\n\n /**\n * Resolve `setup` and `oauth`, then wire OAuth onto the Express app. Runs\n * once; every entry point that needs a built server awaits it, and a failed\n * attempt is retried on the next call.\n *\n * @internal\n */\n ready(): Promise<void> {\n this.readyPromise ??= (async () => {\n if (this.setup) {\n this.context = await this.setup();\n }\n const oauth =\n typeof this.oauthInput === \"function\"\n ? await this.oauthInput(this.context as Awaited<TContext>)\n : await this.oauthInput;\n this.oauthEnabled = Boolean(oauth);\n const sample = this.buildServer();\n if (oauth) {\n this.resolveResourceMetadataUrl = setupOAuth(\n this.expressApp,\n oauth,\n sample.securitySchemesByTool,\n );\n }\n })().catch((error) => {\n this.readyPromise = undefined;\n throw error;\n });\n return this.readyPromise;\n }\n\n /**\n * The underlying Express app. Use this to extend the HTTP server with\n * custom routes, middleware, or settings — e.g.\n * `app.express.get(\"/health\", ...)`.\n *\n * `express.json()` is pre-applied — tune it via the `json` config field,\n * e.g. `new Skybridge({ name, version, json: { limit: \"10mb\" }, handler })`.\n * Register your handlers before `run()`; after `run()`, dev-mode middleware,\n * the `/mcp` route, and the default error handler are appended in that order.\n *\n * Note: Alpic Cloud only routes traffic to `/mcp` — custom routes work\n * locally and on self-hosted deployments.\n */\n get express(): Express {\n return this.expressApp;\n }\n\n /**\n * Build a fresh server for one stateless HTTP request, as\n * `createMcpHandler`'s factory contract requires: the handler runs again so\n * the SDK's handler closures belong to the instance whose protocol era it\n * stamps. Sharing one instance's handler maps instead would bind them to an\n * instance that is never marked, pinning every request to the 2025 codec and\n * letting concurrent callers overwrite each other's negotiated version.\n *\n * Awaits `setup` and the `oauth` input on first use.\n */\n async createServerInstance(): Promise<SdkServer> {\n await this.ready();\n const server = this.buildServer();\n this.instrumentHandlers(server);\n return server.server;\n }\n\n /**\n * Connect a Skybridge app to an MCP transport. Use this when you're\n * embedding Skybridge in a host that already manages its own transport\n * (e.g. stdio for desktop apps); for HTTP, prefer {@link Skybridge.run}\n * which sets the transport up for you.\n */\n async connect(transport: Parameters<SdkServer[\"connect\"]>[0]): Promise<void> {\n const instance = await this.createServerInstance();\n await instance.connect(transport);\n }\n\n /**\n * Register Express middleware on the underlying app. Mirrors `app.use` —\n * pass handlers directly or a path-prefixed handler list. Register before\n * {@link Skybridge.run}; ordering matches Express.\n *\n * Note: Alpic Cloud only routes traffic to `/mcp`. Custom paths work\n * locally and on self-hosted deployments.\n */\n use(...handlers: RequestHandler[]): this;\n use(path: string, ...handlers: RequestHandler[]): this;\n use(\n pathOrHandler: string | RequestHandler,\n ...handlers: RequestHandler[]\n ): this {\n // Branching is load-bearing: Express's `app.use` overloads can't be\n // resolved against a `string | RequestHandler` union, so we narrow.\n if (typeof pathOrHandler === \"string\") {\n this.expressApp.use(pathOrHandler, ...handlers);\n } else {\n this.expressApp.use(pathOrHandler, ...handlers);\n }\n return this;\n }\n\n /**\n * Register Express error-handling middleware to run after the built-in\n * `/mcp` route (or your custom route). Use this to log or transform errors\n * thrown by tool handlers before the default error handler responds.\n *\n * @example\n * ```ts\n * app.useOnError((err, _req, _res, next) => {\n * logger.error(err);\n * next(err);\n * });\n * ```\n */\n useOnError(...handlers: ErrorRequestHandler[]): this;\n useOnError(path: string, ...handlers: ErrorRequestHandler[]): this;\n useOnError(\n pathOrHandler: string | ErrorRequestHandler,\n ...handlers: ErrorRequestHandler[]\n ): this {\n if (typeof pathOrHandler === \"string\") {\n this.errorMiddleware.push({ path: pathOrHandler, handlers });\n } else {\n this.errorMiddleware.push({ handlers: [pathOrHandler, ...handlers] });\n }\n return this;\n }\n\n /**\n * Start the HTTP server. Listens on `process.env.__PORT` (default `3000`),\n * mounts the `/mcp` route, and applies any custom Express middleware\n * registered via {@link Skybridge.use} / {@link Skybridge.useOnError}.\n *\n * On Cloudflare Workers / workerd, returns an object exposing `fetch` so\n * the runtime can bridge incoming requests to the Node HTTP server. On\n * Vercel (`VERCEL === \"1\"`), returns the Express app directly so the\n * serverless function entry can call it as a `(req, res)` handler. On\n * Node, returns `undefined` once listening. When the process was spawned\n * with an IPC channel, the bound port is reported to the parent as\n * `{ type: \"skybridge:listening\", port }`, the readiness signal test runners\n * wait on instead of polling.\n */\n async run(): Promise<\n { fetch: (...args: unknown[]) => unknown } | Express | undefined\n > {\n if (process.env.VERCEL === \"1\") {\n // createApp only reads httpServer inside its dev-only branch\n // (viewsDevServer); under VERCEL=1 + NODE_ENV=production it's a\n // bare object passed to satisfy the required parameter.\n const httpServer = http.createServer();\n await createApp({\n app: this,\n httpServer,\n errorMiddleware: this.errorMiddleware,\n });\n return this.expressApp;\n }\n\n const httpServer = http.createServer();\n const mcpHandler = buildMcpHandler(this);\n\n await createApp({\n app: this,\n httpServer,\n mcpHandler,\n errorMiddleware: this.errorMiddleware,\n });\n\n httpServer.on(\"request\", this.expressApp);\n const intendedPort = parseInt(process.env.__PORT ?? \"3000\", 10);\n await new Promise<void>((resolve, reject) => {\n httpServer.on(\"error\", (error: Error) => {\n console.error(\"Failed to start server:\", error);\n reject(error);\n });\n httpServer.listen(intendedPort, () => {\n resolve();\n });\n });\n\n const { port } = httpServer.address() as AddressInfo;\n process.send?.({ type: \"skybridge:listening\", port });\n\n // On workerd, bridge the Node http server to a Workers fetch handler.\n // The specifier is held in a variable to sidestep tsc's module resolution\n // (`cloudflare:node` only exists under wrangler/workerd).\n if (\n typeof navigator !== \"undefined\" &&\n navigator.userAgent === \"Cloudflare-Workers\"\n ) {\n const cloudflareNode = \"cloudflare:node\";\n const { httpServerHandler } = await import(cloudflareNode);\n return httpServerHandler({ port });\n }\n\n const shutdown = () => {\n // Drop both handlers so a second signal falls through to Node's default\n // (force-quit on a second Ctrl+C while drain is hanging).\n process.off(\"SIGTERM\", shutdown);\n process.off(\"SIGINT\", shutdown);\n mcpHandler.close().catch(() => {});\n httpServer.close(() => process.exit(0));\n // Force exit if connections don't drain in time so the port is still\n // released promptly (e.g. for nodemon restarts).\n setTimeout(() => process.exit(0), 3000).unref();\n };\n process.on(\"SIGTERM\", shutdown);\n process.on(\"SIGINT\", shutdown);\n return undefined;\n }\n\n private buildServer(): McpServer<TTools, TAuthExtra> {\n const server = new McpServer<Record<never, ToolDef>, TAuthExtra>(\n this.serverInfo,\n this.serverOptions,\n { skills: this.skills, oauth: this.oauthEnabled },\n );\n if (this.resolveResourceMetadataUrl) {\n server.setResourceMetadataUrlResolver(this.resolveResourceMetadataUrl);\n }\n const startedAt = performance.now();\n const built = this.handler(server, this.context as Awaited<TContext>);\n const elapsed = performance.now() - startedAt;\n if (typeof (built as { then?: unknown }).then === \"function\") {\n throw new Error(\n \"The Skybridge handler must be synchronous — it runs on every request. Load config or secrets in `setup` instead and read them from the handler's second argument.\",\n );\n }\n if (elapsed > SLOW_HANDLER_THRESHOLD_MS && !this.slowHandlerWarned) {\n this.slowHandlerWarned = true;\n console.warn(\n `The Skybridge handler took ${Math.round(elapsed)}ms — it runs on every request, so this cost is paid per request. Hoist expensive work to module scope or into \\`setup\\`, whose result is passed to the handler.`,\n );\n }\n return built;\n }\n\n private middlewareEntries(\n server: McpServer<Record<never, ToolDef>, TAuthExtra>,\n ): McpMiddlewareEntry[] {\n return [\n ...(this.monitoringEntry ? [this.monitoringEntry] : []),\n ...server.protocolMiddlewareEntries(),\n ];\n }\n\n private instrumentHandlers(\n server: McpServer<Record<never, ToolDef>, TAuthExtra>,\n ): void {\n const entries = this.middlewareEntries(server);\n const { requestHandlers, notificationHandlers } = getHandlerMaps(\n server.server,\n );\n\n const instrumentMap = (\n map: Map<string, (...args: unknown[]) => Promise<unknown>>,\n isNotification: boolean,\n ) => {\n for (const [method, handler] of map) {\n map.set(\n method,\n buildMiddlewareChain(method, isNotification, handler, entries),\n );\n }\n const originalSet = map.set.bind(map);\n map.set = (\n method: string,\n handler: (...args: unknown[]) => Promise<unknown>,\n ) =>\n originalSet(\n method,\n buildMiddlewareChain(method, isNotification, handler, entries),\n );\n };\n\n instrumentMap(requestHandlers, false);\n instrumentMap(notificationHandlers, true);\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { Skybridge } from "./app.js";
|
|
4
|
+
const oauthConfig = {
|
|
5
|
+
oauthMetadata: {
|
|
6
|
+
issuer: "https://issuer.example.com",
|
|
7
|
+
authorization_endpoint: "https://issuer.example.com/authorize",
|
|
8
|
+
token_endpoint: "https://issuer.example.com/token",
|
|
9
|
+
response_types_supported: ["code"],
|
|
10
|
+
},
|
|
11
|
+
verifier: {
|
|
12
|
+
verifyAccessToken: async () => ({
|
|
13
|
+
token: "t",
|
|
14
|
+
clientId: "c",
|
|
15
|
+
scopes: [],
|
|
16
|
+
extra: {},
|
|
17
|
+
}),
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
describe("Skybridge setup and handler", () => {
|
|
21
|
+
it("resolves setup once and passes the context to the per-request handler", async () => {
|
|
22
|
+
let loads = 0;
|
|
23
|
+
const app = new Skybridge({
|
|
24
|
+
name: "test",
|
|
25
|
+
version: "1.0.0",
|
|
26
|
+
setup: async () => {
|
|
27
|
+
loads++;
|
|
28
|
+
return { greeting: await Promise.resolve("hello") };
|
|
29
|
+
},
|
|
30
|
+
handler: (server, { greeting }) => server.registerTool({ name: "greet", description: "greet" }, async () => ({ content: [{ type: "text", text: greeting }] })),
|
|
31
|
+
});
|
|
32
|
+
expect(loads).toBe(0);
|
|
33
|
+
const call = async () => {
|
|
34
|
+
const client = new Client({ name: "client", version: "1.0.0" });
|
|
35
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
36
|
+
await app.connect(serverTransport);
|
|
37
|
+
await client.connect(clientTransport);
|
|
38
|
+
const result = await client.callTool({ name: "greet" });
|
|
39
|
+
await client.close();
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
expect(await call()).toMatchObject({
|
|
43
|
+
content: [{ type: "text", text: "hello" }],
|
|
44
|
+
});
|
|
45
|
+
expect(await call()).toMatchObject({
|
|
46
|
+
content: [{ type: "text", text: "hello" }],
|
|
47
|
+
});
|
|
48
|
+
expect(loads).toBe(1);
|
|
49
|
+
});
|
|
50
|
+
it("retries setup after a failed first resolution", async () => {
|
|
51
|
+
let attempts = 0;
|
|
52
|
+
const app = new Skybridge({
|
|
53
|
+
name: "test",
|
|
54
|
+
version: "1.0.0",
|
|
55
|
+
setup: async () => {
|
|
56
|
+
attempts++;
|
|
57
|
+
if (attempts === 1) {
|
|
58
|
+
throw new Error("transient");
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
handler: (server) => server,
|
|
62
|
+
});
|
|
63
|
+
const [, first] = InMemoryTransport.createLinkedPair();
|
|
64
|
+
await expect(app.connect(first)).rejects.toThrow("transient");
|
|
65
|
+
const [, second] = InMemoryTransport.createLinkedPair();
|
|
66
|
+
await expect(app.connect(second)).resolves.toBeUndefined();
|
|
67
|
+
expect(attempts).toBe(2);
|
|
68
|
+
});
|
|
69
|
+
it("rejects a handler that returns a promise", async () => {
|
|
70
|
+
const app = new Skybridge({
|
|
71
|
+
name: "test",
|
|
72
|
+
version: "1.0.0",
|
|
73
|
+
// @ts-expect-error the handler runs per request and must stay synchronous
|
|
74
|
+
handler: async (server) => server,
|
|
75
|
+
});
|
|
76
|
+
const [, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
77
|
+
await expect(app.connect(serverTransport)).rejects.toThrow(/must be synchronous/);
|
|
78
|
+
});
|
|
79
|
+
it("resolves an oauth function with the setup context before wiring the routes", async () => {
|
|
80
|
+
let seen;
|
|
81
|
+
const app = new Skybridge({
|
|
82
|
+
name: "test",
|
|
83
|
+
version: "1.0.0",
|
|
84
|
+
setup: () => ({ issuer: "https://issuer.example.com" }),
|
|
85
|
+
oauth: (context) => {
|
|
86
|
+
seen = context;
|
|
87
|
+
return oauthConfig;
|
|
88
|
+
},
|
|
89
|
+
handler: (server) => server,
|
|
90
|
+
});
|
|
91
|
+
expect(seen).toBeUndefined();
|
|
92
|
+
const [, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
93
|
+
await app.connect(serverTransport);
|
|
94
|
+
expect(seen).toEqual({ issuer: "https://issuer.example.com" });
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=app.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.test.js","sourceRoot":"","sources":["../../src/server/app.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACzE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAErC,MAAM,WAAW,GAAG;IAClB,aAAa,EAAE;QACb,MAAM,EAAE,4BAA4B;QACpC,sBAAsB,EAAE,sCAAsC;QAC9D,cAAc,EAAE,kCAAkC;QAClD,wBAAwB,EAAE,CAAC,MAAM,CAAC;KACnC;IACD,QAAQ,EAAE;QACR,iBAAiB,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;YAC9B,KAAK,EAAE,GAAG;YACV,QAAQ,EAAE,GAAG;YACb,MAAM,EAAE,EAAE;YACV,KAAK,EAAE,EAAE;SACV,CAAC;KACH;CACF,CAAC;AAEF,QAAQ,CAAC,6BAA6B,EAAE,GAAG,EAAE;IAC3C,EAAE,CAAC,uEAAuE,EAAE,KAAK,IAAI,EAAE;QACrF,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC;YACxB,IAAI,EAAE,MAAM;YACZ,OAAO,EAAE,OAAO;YAChB,KAAK,EAAE,KAAK,IAAI,EAAE;gBAChB,KAAK,EAAE,CAAC;gBACR,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YACtD,CAAC;YACD,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,QAAQ,EAAE,EAAE,EAAE,CAChC,MAAM,CAAC,YAAY,CACjB,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,EACvC,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAC9D;SACJ,CAAC,CAAC;QAEH,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAEtB,MAAM,IAAI,GAAG,KAAK,IAAI,EAAE;YACtB,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;YAChE,MAAM,CAAC,eAAe,EAAE,eAAe,CAAC,GACtC,iBAAiB,CAAC,gBAAgB,EAAE,CAAC;YACvC,MAAM,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;YACnC,MAAM,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;YACtC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YACxD,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACrB,OAAO,MAAM,CAAC;QAChB,CAAC,CAAC;QAEF,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,aAAa,CAAC;YACjC,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;SAC3C,CAAC,CAAC;QACH,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,aAAa,CAAC;YACjC,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;SAC3C,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;QAC7D,IAAI,QAAQ,GAAG,CAAC,CAAC;QACjB,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC;YACxB,IAAI,EAAE,MAAM;YACZ,OAAO,EAAE,OAAO;YAChB,KAAK,EAAE,KAAK,IAAI,EAAE;gBAChB,QAAQ,EAAE,CAAC;gBACX,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;oBACnB,MAAM,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC;gBAC/B,CAAC;YACH,CAAC;YACD,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM;SAC5B,CAAC,CAAC;QAEH,MAAM,CAAC,EAAE,KAAK,CAAC,GAAG,iBAAiB,CAAC,gBAAgB,EAAE,CAAC;QACvD,MAAM,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QAE9D,MAAM,CAAC,EAAE,MAAM,CAAC,GAAG,iBAAiB,CAAC,gBAAgB,EAAE,CAAC;QACxD,MAAM,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAC;QAC3D,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC;YACxB,IAAI,EAAE,MAAM;YACZ,OAAO,EAAE,OAAO;YAChB,0EAA0E;YAC1E,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM;SAClC,CAAC,CAAC;QACH,MAAM,CAAC,EAAE,eAAe,CAAC,GAAG,iBAAiB,CAAC,gBAAgB,EAAE,CAAC;QACjE,MAAM,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CACxD,qBAAqB,CACtB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4EAA4E,EAAE,KAAK,IAAI,EAAE;QAC1F,IAAI,IAAa,CAAC;QAClB,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC;YACxB,IAAI,EAAE,MAAM;YACZ,OAAO,EAAE,OAAO;YAChB,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,4BAA4B,EAAE,CAAC;YACvD,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE;gBACjB,IAAI,GAAG,OAAO,CAAC;gBACf,OAAO,WAAW,CAAC;YACrB,CAAC;YACD,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM;SAC5B,CAAC,CAAC;QAEH,MAAM,CAAC,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC;QAC7B,MAAM,CAAC,EAAE,eAAe,CAAC,GAAG,iBAAiB,CAAC,gBAAgB,EAAE,CAAC;QACjE,MAAM,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QACnC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,4BAA4B,EAAE,CAAC,CAAC;IACjE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { Client, InMemoryTransport } from \"@modelcontextprotocol/client\";\nimport { describe, expect, it } from \"vitest\";\nimport { Skybridge } from \"./app.js\";\n\nconst oauthConfig = {\n oauthMetadata: {\n issuer: \"https://issuer.example.com\",\n authorization_endpoint: \"https://issuer.example.com/authorize\",\n token_endpoint: \"https://issuer.example.com/token\",\n response_types_supported: [\"code\"],\n },\n verifier: {\n verifyAccessToken: async () => ({\n token: \"t\",\n clientId: \"c\",\n scopes: [],\n extra: {},\n }),\n },\n};\n\ndescribe(\"Skybridge setup and handler\", () => {\n it(\"resolves setup once and passes the context to the per-request handler\", async () => {\n let loads = 0;\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n setup: async () => {\n loads++;\n return { greeting: await Promise.resolve(\"hello\") };\n },\n handler: (server, { greeting }) =>\n server.registerTool(\n { name: \"greet\", description: \"greet\" },\n async () => ({ content: [{ type: \"text\", text: greeting }] }),\n ),\n });\n\n expect(loads).toBe(0);\n\n const call = async () => {\n const client = new Client({ name: \"client\", version: \"1.0.0\" });\n const [clientTransport, serverTransport] =\n InMemoryTransport.createLinkedPair();\n await app.connect(serverTransport);\n await client.connect(clientTransport);\n const result = await client.callTool({ name: \"greet\" });\n await client.close();\n return result;\n };\n\n expect(await call()).toMatchObject({\n content: [{ type: \"text\", text: \"hello\" }],\n });\n expect(await call()).toMatchObject({\n content: [{ type: \"text\", text: \"hello\" }],\n });\n expect(loads).toBe(1);\n });\n\n it(\"retries setup after a failed first resolution\", async () => {\n let attempts = 0;\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n setup: async () => {\n attempts++;\n if (attempts === 1) {\n throw new Error(\"transient\");\n }\n },\n handler: (server) => server,\n });\n\n const [, first] = InMemoryTransport.createLinkedPair();\n await expect(app.connect(first)).rejects.toThrow(\"transient\");\n\n const [, second] = InMemoryTransport.createLinkedPair();\n await expect(app.connect(second)).resolves.toBeUndefined();\n expect(attempts).toBe(2);\n });\n\n it(\"rejects a handler that returns a promise\", async () => {\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n // @ts-expect-error the handler runs per request and must stay synchronous\n handler: async (server) => server,\n });\n const [, serverTransport] = InMemoryTransport.createLinkedPair();\n await expect(app.connect(serverTransport)).rejects.toThrow(\n /must be synchronous/,\n );\n });\n\n it(\"resolves an oauth function with the setup context before wiring the routes\", async () => {\n let seen: unknown;\n const app = new Skybridge({\n name: \"test\",\n version: \"1.0.0\",\n setup: () => ({ issuer: \"https://issuer.example.com\" }),\n oauth: (context) => {\n seen = context;\n return oauthConfig;\n },\n handler: (server) => server,\n });\n\n expect(seen).toBeUndefined();\n const [, serverTransport] = InMemoryTransport.createLinkedPair();\n await app.connect(serverTransport);\n expect(seen).toEqual({ issuer: \"https://issuer.example.com\" });\n });\n});\n"]}
|