@every-app/sdk 0.1.14 → 0.2.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/dist/internal/identity.d.ts +73 -0
- package/dist/internal/identity.d.ts.map +1 -0
- package/dist/internal/identity.js +192 -0
- package/dist/internal/index.d.ts +3 -0
- package/dist/internal/index.d.ts.map +1 -0
- package/dist/internal/index.js +2 -0
- package/dist/internal/protocol.d.ts +111 -0
- package/dist/internal/protocol.d.ts.map +1 -0
- package/dist/internal/protocol.js +139 -0
- package/dist/server/everyApp.d.ts +55 -0
- package/dist/server/everyApp.d.ts.map +1 -0
- package/dist/server/everyApp.js +121 -0
- package/dist/server/gatewayFetch.d.ts +21 -0
- package/dist/server/gatewayFetch.d.ts.map +1 -0
- package/dist/server/gatewayFetch.js +70 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +8 -0
- package/dist/server/mcp.d.ts +66 -0
- package/dist/server/mcp.d.ts.map +1 -0
- package/dist/server/mcp.js +147 -0
- package/package.json +20 -43
- package/dist/cloudflare/getLocalD1Url.d.ts +0 -2
- package/dist/cloudflare/getLocalD1Url.d.ts.map +0 -1
- package/dist/cloudflare/getLocalD1Url.js +0 -40
- package/dist/cloudflare/index.d.ts +0 -2
- package/dist/cloudflare/index.d.ts.map +0 -1
- package/dist/cloudflare/index.js +0 -1
- package/dist/cloudflare/lazyInit.d.ts +0 -22
- package/dist/cloudflare/lazyInit.d.ts.map +0 -1
- package/dist/cloudflare/lazyInit.js +0 -65
- package/dist/cloudflare/server/gateway.d.ts +0 -29
- package/dist/cloudflare/server/gateway.d.ts.map +0 -1
- package/dist/cloudflare/server/gateway.js +0 -69
- package/dist/cloudflare/server/index.d.ts +0 -3
- package/dist/cloudflare/server/index.d.ts.map +0 -1
- package/dist/cloudflare/server/index.js +0 -2
- package/dist/core/authenticatedFetch.d.ts +0 -9
- package/dist/core/authenticatedFetch.d.ts.map +0 -1
- package/dist/core/authenticatedFetch.js +0 -31
- package/dist/core/index.d.ts +0 -4
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/index.js +0 -2
- package/dist/core/sessionManager.d.ts +0 -58
- package/dist/core/sessionManager.d.ts.map +0 -1
- package/dist/core/sessionManager.js +0 -357
- package/dist/shared/bypassGatewayLocalOnly.d.ts +0 -15
- package/dist/shared/bypassGatewayLocalOnly.d.ts.map +0 -1
- package/dist/shared/bypassGatewayLocalOnly.js +0 -45
- package/dist/shared/parseMessagePayload.d.ts +0 -3
- package/dist/shared/parseMessagePayload.d.ts.map +0 -1
- package/dist/shared/parseMessagePayload.js +0 -18
- package/dist/tanstack/EmbeddedAppProvider.d.ts +0 -30
- package/dist/tanstack/EmbeddedAppProvider.d.ts.map +0 -1
- package/dist/tanstack/EmbeddedAppProvider.js +0 -57
- package/dist/tanstack/GatewayRequiredError.d.ts +0 -20
- package/dist/tanstack/GatewayRequiredError.d.ts.map +0 -1
- package/dist/tanstack/GatewayRequiredError.js +0 -91
- package/dist/tanstack/_internal/useEveryAppSession.d.ts +0 -18
- package/dist/tanstack/_internal/useEveryAppSession.d.ts.map +0 -1
- package/dist/tanstack/_internal/useEveryAppSession.js +0 -47
- package/dist/tanstack/index.d.ts +0 -4
- package/dist/tanstack/index.d.ts.map +0 -1
- package/dist/tanstack/index.js +0 -3
- package/dist/tanstack/server/authConfig.d.ts +0 -3
- package/dist/tanstack/server/authConfig.d.ts.map +0 -1
- package/dist/tanstack/server/authConfig.js +0 -13
- package/dist/tanstack/server/authenticateRequest.d.ts +0 -31
- package/dist/tanstack/server/authenticateRequest.d.ts.map +0 -1
- package/dist/tanstack/server/authenticateRequest.js +0 -154
- package/dist/tanstack/server/index.d.ts +0 -4
- package/dist/tanstack/server/index.d.ts.map +0 -1
- package/dist/tanstack/server/index.js +0 -2
- package/dist/tanstack/server/types.d.ts +0 -5
- package/dist/tanstack/server/types.d.ts.map +0 -1
- package/dist/tanstack/server/types.js +0 -1
- package/dist/tanstack/useEveryAppRouter.d.ts +0 -7
- package/dist/tanstack/useEveryAppRouter.d.ts.map +0 -1
- package/dist/tanstack/useEveryAppRouter.js +0 -66
- package/dist/tanstack/useSessionTokenClientMiddleware.d.ts +0 -2
- package/dist/tanstack/useSessionTokenClientMiddleware.d.ts.map +0 -1
- package/dist/tanstack/useSessionTokenClientMiddleware.js +0 -29
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `everyApp()` — the app-side identity boundary.
|
|
3
|
+
*
|
|
4
|
+
* import manifest from "../everyapp.config.js";
|
|
5
|
+
* export default everyApp(handler, manifest);
|
|
6
|
+
*
|
|
7
|
+
* The wrapper verifies the gateway-injected identity before app code runs,
|
|
8
|
+
* stores the result on the original Request, and then delegates to the
|
|
9
|
+
* worker-style handler.
|
|
10
|
+
*/
|
|
11
|
+
import { APP_ID_ENV, ConfigurationError, DEV_ENV, IdentityError, ISSUER_ENV, PUBLIC_KEYS_ENV, getIdentityFromRequest, } from "../internal/index.js";
|
|
12
|
+
export { ConfigurationError, IdentityError };
|
|
13
|
+
const requestIdentities = new WeakMap();
|
|
14
|
+
function envValue(env, key) {
|
|
15
|
+
if (!env || typeof env !== "object")
|
|
16
|
+
return undefined;
|
|
17
|
+
const value = env[key];
|
|
18
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
19
|
+
}
|
|
20
|
+
function configFrom(manifest, options = {}) {
|
|
21
|
+
return {
|
|
22
|
+
audience: manifest.id,
|
|
23
|
+
issuer: options.issuer,
|
|
24
|
+
keysEnvVar: options.keysEnvVar ?? PUBLIC_KEYS_ENV,
|
|
25
|
+
publicKeys: options.publicKeys,
|
|
26
|
+
allowDevIdentities: options.allowDevIdentities,
|
|
27
|
+
now: options.now,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function unauthorized(message) {
|
|
31
|
+
return Response.json({ error: "unauthenticated", message }, { status: 401 });
|
|
32
|
+
}
|
|
33
|
+
function misconfigured(message) {
|
|
34
|
+
return Response.json({ error: "misconfigured", message }, { status: 500 });
|
|
35
|
+
}
|
|
36
|
+
async function resolveIdentity(request, env, config) {
|
|
37
|
+
const devEnv = env && typeof env === "object"
|
|
38
|
+
? env[DEV_ENV]
|
|
39
|
+
: undefined;
|
|
40
|
+
return getIdentityFromRequest(request, {
|
|
41
|
+
audience: config.audience,
|
|
42
|
+
issuer: config.issuer ?? envValue(env, ISSUER_ENV),
|
|
43
|
+
publicKeys: config.publicKeys,
|
|
44
|
+
env: env,
|
|
45
|
+
keysEnvVar: config.keysEnvVar,
|
|
46
|
+
allowDevIdentities: config.allowDevIdentities ??
|
|
47
|
+
(typeof devEnv === "string" &&
|
|
48
|
+
(devEnv === "1" || devEnv.toLowerCase() === "true")),
|
|
49
|
+
now: config.now,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function rememberIdentity(request, identity) {
|
|
53
|
+
requestIdentities.set(request, identity);
|
|
54
|
+
}
|
|
55
|
+
function helperConfigFromEnv(env) {
|
|
56
|
+
const audience = envValue(env, APP_ID_ENV);
|
|
57
|
+
if (!audience) {
|
|
58
|
+
throw new ConfigurationError(`${APP_ID_ENV} is not set — the CLI must inject the app id for identity verification.`);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
audience,
|
|
62
|
+
keysEnvVar: PUBLIC_KEYS_ENV,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Return the verified Every App user for this request, or null for a declared
|
|
67
|
+
* public request. Uses the request cache first, then verifies the identity
|
|
68
|
+
* headers from the app id and public keys configured in env.
|
|
69
|
+
*/
|
|
70
|
+
export async function getEveryAppUser(request, env) {
|
|
71
|
+
const cached = requestIdentities.get(request);
|
|
72
|
+
if (cached)
|
|
73
|
+
return cached.user;
|
|
74
|
+
const identity = await resolveIdentity(request, env, helperConfigFromEnv(env));
|
|
75
|
+
rememberIdentity(request, identity);
|
|
76
|
+
return identity.user;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Return the verified user, or throw the same 401 JSON Response shape as
|
|
80
|
+
* everyApp() when the request is public or unauthenticated.
|
|
81
|
+
*/
|
|
82
|
+
export async function requireEveryAppUser(request, env) {
|
|
83
|
+
try {
|
|
84
|
+
const user = await getEveryAppUser(request, env);
|
|
85
|
+
if (!user) {
|
|
86
|
+
throw new IdentityError("this request does not have an authenticated user");
|
|
87
|
+
}
|
|
88
|
+
return user;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error instanceof IdentityError) {
|
|
92
|
+
throw unauthorized(error.message);
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function hasScope(user, scope) {
|
|
98
|
+
return user.scopes.includes("*") || user.scopes.includes(scope);
|
|
99
|
+
}
|
|
100
|
+
export function everyApp(handler, manifest, options = {}) {
|
|
101
|
+
const config = configFrom(manifest, options);
|
|
102
|
+
return {
|
|
103
|
+
async fetch(request, env, ctx) {
|
|
104
|
+
let identity;
|
|
105
|
+
try {
|
|
106
|
+
identity = await resolveIdentity(request, env, config);
|
|
107
|
+
rememberIdentity(request, identity);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
if (err instanceof ConfigurationError) {
|
|
111
|
+
console.error(err);
|
|
112
|
+
return misconfigured(err.message);
|
|
113
|
+
}
|
|
114
|
+
if (err instanceof IdentityError)
|
|
115
|
+
return unauthorized(err.message);
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
return handler(request, env, ctx, identity.user);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface GatewayServiceBinding {
|
|
2
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
3
|
+
}
|
|
4
|
+
export interface GatewayFetchEnv {
|
|
5
|
+
EVERY_APP_GATEWAY?: GatewayServiceBinding;
|
|
6
|
+
EVERYAPP_DEV?: string | boolean;
|
|
7
|
+
OPENAI_API_KEY?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface CreateGatewayFetchOptions {
|
|
10
|
+
env: GatewayFetchEnv;
|
|
11
|
+
/** The inbound app request whose verified identity may be forwarded. */
|
|
12
|
+
request: Request;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Create an OpenAI-compatible fetch function backed by the private gateway
|
|
16
|
+
* service binding. In local dev only, the developer's own OPENAI_API_KEY is a
|
|
17
|
+
* direct-provider fallback when no binding exists.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createGatewayFetch({ env, request: inboundRequest, }: CreateGatewayFetchOptions): typeof fetch;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=gatewayFetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gatewayFetch.d.ts","sourceRoot":"","sources":["../../src/server/gatewayFetch.ts"],"names":[],"mappings":"AAMA,UAAU,qBAAqB;IAC7B,KAAK,CAAC,KAAK,EAAE,WAAW,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACxE;AAED,MAAM,WAAW,eAAe;IAC9B,iBAAiB,CAAC,EAAE,qBAAqB,CAAC;IAC1C,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAChC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,yBAAyB;IACxC,GAAG,EAAE,eAAe,CAAC;IACrB,wEAAwE;IACxE,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,GAAG,EACH,OAAO,EAAE,cAAc,GACxB,EAAE,yBAAyB,GAAG,OAAO,KAAK,CAyC1C"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ConfigurationError, IDENTITY_HEADER } from "../internal/index.js";
|
|
2
|
+
const BINDING_ORIGIN = "http://every-app-gateway.internal";
|
|
3
|
+
const OPENAI_UPSTREAM_ORIGIN = "https://api.openai.com";
|
|
4
|
+
const OPENAI_PROXY_PREFIX = "/api/ai/openai";
|
|
5
|
+
/**
|
|
6
|
+
* Create an OpenAI-compatible fetch function backed by the private gateway
|
|
7
|
+
* service binding. In local dev only, the developer's own OPENAI_API_KEY is a
|
|
8
|
+
* direct-provider fallback when no binding exists.
|
|
9
|
+
*/
|
|
10
|
+
export function createGatewayFetch({ env, request: inboundRequest, }) {
|
|
11
|
+
const binding = env.EVERY_APP_GATEWAY;
|
|
12
|
+
const devProviderKey = env.EVERYAPP_DEV
|
|
13
|
+
? env.OPENAI_API_KEY?.trim()
|
|
14
|
+
: undefined;
|
|
15
|
+
if (binding && typeof binding.fetch !== "function") {
|
|
16
|
+
throw new ConfigurationError("EVERY_APP_GATEWAY is configured but is not a valid service binding.");
|
|
17
|
+
}
|
|
18
|
+
if (!binding && !devProviderKey) {
|
|
19
|
+
throw new ConfigurationError("AI gateway unavailable: deploy with the EVERY_APP_GATEWAY binding, or set OPENAI_API_KEY in .dev.vars for direct local development.");
|
|
20
|
+
}
|
|
21
|
+
return async (input, init) => {
|
|
22
|
+
const outbound = new Request(input, init);
|
|
23
|
+
const headers = new Headers(outbound.headers);
|
|
24
|
+
headers.delete("authorization");
|
|
25
|
+
const identity = inboundRequest.headers.get(IDENTITY_HEADER);
|
|
26
|
+
if (identity)
|
|
27
|
+
headers.set(IDENTITY_HEADER, identity);
|
|
28
|
+
else
|
|
29
|
+
headers.delete(IDENTITY_HEADER);
|
|
30
|
+
if (binding) {
|
|
31
|
+
const target = new URL(outbound.url);
|
|
32
|
+
const bindingRequest = streamingRequest(`${BINDING_ORIGIN}${target.pathname}${target.search}`, outbound, headers);
|
|
33
|
+
return binding.fetch(bindingRequest);
|
|
34
|
+
}
|
|
35
|
+
const providerUrl = directOpenAiUrl(outbound.url);
|
|
36
|
+
headers.delete(IDENTITY_HEADER);
|
|
37
|
+
headers.set("authorization", `Bearer ${devProviderKey}`);
|
|
38
|
+
return fetch(streamingRequest(providerUrl, outbound, headers));
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function streamingRequest(url, source, headers) {
|
|
42
|
+
const body = canIncludeBody(source.method) ? source.body : undefined;
|
|
43
|
+
const init = {
|
|
44
|
+
method: source.method,
|
|
45
|
+
headers,
|
|
46
|
+
body,
|
|
47
|
+
signal: source.signal,
|
|
48
|
+
...(body ? { duplex: "half" } : {}),
|
|
49
|
+
};
|
|
50
|
+
return new Request(url, init);
|
|
51
|
+
}
|
|
52
|
+
function directOpenAiUrl(input) {
|
|
53
|
+
const incoming = new URL(input);
|
|
54
|
+
if (incoming.pathname !== OPENAI_PROXY_PREFIX &&
|
|
55
|
+
!incoming.pathname.startsWith(`${OPENAI_PROXY_PREFIX}/`)) {
|
|
56
|
+
throw new ConfigurationError(`Direct local provider fallback only supports ${OPENAI_PROXY_PREFIX} paths.`);
|
|
57
|
+
}
|
|
58
|
+
const upstreamPath = incoming.pathname.slice(OPENAI_PROXY_PREFIX.length) || "/";
|
|
59
|
+
if (!upstreamPath.startsWith("/") || upstreamPath.startsWith("//")) {
|
|
60
|
+
throw new ConfigurationError("Invalid OpenAI proxy path.");
|
|
61
|
+
}
|
|
62
|
+
const upstream = new URL(OPENAI_UPSTREAM_ORIGIN);
|
|
63
|
+
upstream.pathname = upstreamPath;
|
|
64
|
+
upstream.search = incoming.search;
|
|
65
|
+
return upstream.toString();
|
|
66
|
+
}
|
|
67
|
+
function canIncludeBody(method) {
|
|
68
|
+
const normalized = method.toUpperCase();
|
|
69
|
+
return normalized !== "GET" && normalized !== "HEAD";
|
|
70
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @every-app/sdk/server — the server-side surface.
|
|
3
|
+
*
|
|
4
|
+
* There is intentionally no client-side export.
|
|
5
|
+
*/
|
|
6
|
+
export { ConfigurationError, IdentityError, everyApp, getEveryAppUser, hasScope, requireEveryAppUser, type EveryAppOptions, type EveryAppManifestLike, type EveryAppHandler, type EveryAppUser, type IdentityResult, } from "./everyApp.js";
|
|
7
|
+
export { createGatewayFetch, type CreateGatewayFetchOptions, type GatewayFetchEnv, } from "./gatewayFetch.js";
|
|
8
|
+
export { createMcpHandler, type CreateMcpHandlerOptions, type McpToolContext, type McpToolDefinition, type McpRequestHandler, } from "./mcp.js";
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,QAAQ,EACR,eAAe,EACf,QAAQ,EACR,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,EAC9B,KAAK,eAAe,GACrB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,gBAAgB,EAChB,KAAK,uBAAuB,EAC5B,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,GACvB,MAAM,UAAU,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @every-app/sdk/server — the server-side surface.
|
|
3
|
+
*
|
|
4
|
+
* There is intentionally no client-side export.
|
|
5
|
+
*/
|
|
6
|
+
export { ConfigurationError, IdentityError, everyApp, getEveryAppUser, hasScope, requireEveryAppUser, } from "./everyApp.js";
|
|
7
|
+
export { createGatewayFetch, } from "./gatewayFetch.js";
|
|
8
|
+
export { createMcpHandler, } from "./mcp.js";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `createMcpHandler()` — a stateless Streamable HTTP MCP server handler.
|
|
3
|
+
*
|
|
4
|
+
* const mcp = createMcpHandler({
|
|
5
|
+
* name: "my-app",
|
|
6
|
+
* tools: {
|
|
7
|
+
* list_items: {
|
|
8
|
+
* description: "List the user's items",
|
|
9
|
+
* inputSchema: { type: "object", properties: { limit: { type: "number" } } },
|
|
10
|
+
* handler: async (args, { user, env }) => listItems(user.id, args.limit),
|
|
11
|
+
* },
|
|
12
|
+
* },
|
|
13
|
+
* });
|
|
14
|
+
* export default everyApp((request, env, ctx, user) => {
|
|
15
|
+
* if (new URL(request.url).pathname === "/mcp") return mcp(request, env, ctx, user);
|
|
16
|
+
* ...
|
|
17
|
+
* }, manifest);
|
|
18
|
+
*
|
|
19
|
+
* The gateway authenticates the caller and injects the identity JWT; this
|
|
20
|
+
* handler only consumes the verified `EveryAppUser`. Each POST carries one
|
|
21
|
+
* self-contained JSON-RPC message (no sessions, no SSE), so any isolate can
|
|
22
|
+
* serve any request.
|
|
23
|
+
*/
|
|
24
|
+
import type { EveryAppUser } from "../internal/index.js";
|
|
25
|
+
import { type ExecutionContextLike } from "./everyApp.js";
|
|
26
|
+
export interface McpToolContext<TEnv = Record<string, unknown>> {
|
|
27
|
+
user: EveryAppUser;
|
|
28
|
+
env: TEnv;
|
|
29
|
+
request: Request;
|
|
30
|
+
ctx: ExecutionContextLike;
|
|
31
|
+
}
|
|
32
|
+
export interface McpToolDefinition<TEnv = Record<string, unknown>> {
|
|
33
|
+
description?: string;
|
|
34
|
+
/** JSON Schema for the tool's arguments. Defaults to an empty object schema. */
|
|
35
|
+
inputSchema?: Record<string, unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* Scope required to list or call this tool, checked against the scopes the
|
|
38
|
+
* credential was granted (`user.scopes`). Omit to allow any authenticated user.
|
|
39
|
+
*/
|
|
40
|
+
scope?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Tool implementation. Return a string or JSON-serializable value (wrapped as
|
|
43
|
+
* text content), or a full `{ content: [...] }` result. Thrown errors become
|
|
44
|
+
* `isError: true` tool results, not protocol errors.
|
|
45
|
+
*/
|
|
46
|
+
handler: (args: Record<string, unknown>, context: McpToolContext<TEnv>) => unknown;
|
|
47
|
+
}
|
|
48
|
+
export interface CreateMcpHandlerOptions<TEnv = Record<string, unknown>> {
|
|
49
|
+
/** Server name reported to MCP clients. */
|
|
50
|
+
name: string;
|
|
51
|
+
version?: string;
|
|
52
|
+
/** Optional usage hints surfaced to clients after initialize. */
|
|
53
|
+
instructions?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Browser origins allowed to call this endpoint, in addition to the
|
|
56
|
+
* endpoint's own origin. Non-browser clients send no Origin header and are
|
|
57
|
+
* unaffected; any other present Origin is rejected with 403 (the Streamable
|
|
58
|
+
* HTTP spec's DNS-rebinding defense).
|
|
59
|
+
*/
|
|
60
|
+
allowedOrigins?: string[];
|
|
61
|
+
tools: Record<string, McpToolDefinition<TEnv>>;
|
|
62
|
+
}
|
|
63
|
+
/** Matches the everyApp() handler signature so it can be delegated to directly. */
|
|
64
|
+
export type McpRequestHandler<TEnv = Record<string, unknown>> = (request: Request, env: TEnv, ctx: ExecutionContextLike, user?: EveryAppUser | null) => Promise<Response>;
|
|
65
|
+
export declare function createMcpHandler<TEnv = Record<string, unknown>>(options: CreateMcpHandlerOptions<TEnv>): McpRequestHandler<TEnv>;
|
|
66
|
+
//# sourceMappingURL=mcp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/server/mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,eAAe,CAAC;AAUvB,MAAM,WAAW,cAAc,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC5D,IAAI,EAAE,YAAY,CAAC;IACnB,GAAG,EAAE,IAAI,CAAC;IACV,OAAO,EAAE,OAAO,CAAC;IACjB,GAAG,EAAE,oBAAoB,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAC/D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACtC;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,OAAO,EAAE,CACP,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC,KAC1B,OAAO,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IACrE,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;CAChD;AAED,mFAAmF;AACnF,MAAM,MAAM,iBAAiB,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAC9D,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,IAAI,EACT,GAAG,EAAE,oBAAoB,EACzB,IAAI,CAAC,EAAE,YAAY,GAAG,IAAI,KACvB,OAAO,CAAC,QAAQ,CAAC,CAAC;AAiDvB,wBAAgB,gBAAgB,CAAC,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7D,OAAO,EAAE,uBAAuB,CAAC,IAAI,CAAC,GACrC,iBAAiB,CAAC,IAAI,CAAC,CA8IzB"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { getEveryAppUser, hasScope, } from "./everyApp.js";
|
|
2
|
+
/** Protocol revisions this handler accepts; initialize echoes the client's when supported. */
|
|
3
|
+
const SUPPORTED_PROTOCOL_VERSIONS = [
|
|
4
|
+
"2025-03-26",
|
|
5
|
+
"2025-06-18",
|
|
6
|
+
"2025-11-25",
|
|
7
|
+
];
|
|
8
|
+
const DEFAULT_PROTOCOL_VERSION = "2025-06-18";
|
|
9
|
+
function userHasScope(user, scope) {
|
|
10
|
+
return !scope || hasScope(user, scope);
|
|
11
|
+
}
|
|
12
|
+
function jsonRpcResult(id, result) {
|
|
13
|
+
return Response.json({ jsonrpc: "2.0", id, result });
|
|
14
|
+
}
|
|
15
|
+
function jsonRpcError(id, code, message) {
|
|
16
|
+
return Response.json({ jsonrpc: "2.0", id, error: { code, message } });
|
|
17
|
+
}
|
|
18
|
+
function accepted() {
|
|
19
|
+
return new Response(null, { status: 202 });
|
|
20
|
+
}
|
|
21
|
+
function toCallToolResult(value) {
|
|
22
|
+
if (value !== null &&
|
|
23
|
+
typeof value === "object" &&
|
|
24
|
+
Array.isArray(value.content)) {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
const text = typeof value === "string" ? value : JSON.stringify(value ?? null);
|
|
28
|
+
return { content: [{ type: "text", text }] };
|
|
29
|
+
}
|
|
30
|
+
function negotiateProtocolVersion(params) {
|
|
31
|
+
const requested = params && typeof params === "object"
|
|
32
|
+
? params.protocolVersion
|
|
33
|
+
: undefined;
|
|
34
|
+
return typeof requested === "string" &&
|
|
35
|
+
SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
|
|
36
|
+
? requested
|
|
37
|
+
: DEFAULT_PROTOCOL_VERSION;
|
|
38
|
+
}
|
|
39
|
+
export function createMcpHandler(options) {
|
|
40
|
+
const { name, version = "0.0.0", instructions, allowedOrigins = [], tools, } = options;
|
|
41
|
+
return async (request, env, ctx, user) => {
|
|
42
|
+
if (request.method !== "POST") {
|
|
43
|
+
return new Response(null, { status: 405, headers: { allow: "POST" } });
|
|
44
|
+
}
|
|
45
|
+
const origin = request.headers.get("origin");
|
|
46
|
+
if (origin &&
|
|
47
|
+
origin !== new URL(request.url).origin &&
|
|
48
|
+
!allowedOrigins.includes(origin)) {
|
|
49
|
+
return Response.json({ error: "forbidden_origin" }, { status: 403 });
|
|
50
|
+
}
|
|
51
|
+
const resolvedUser = user !== undefined ? user : await getEveryAppUser(request, env);
|
|
52
|
+
if (!resolvedUser) {
|
|
53
|
+
return Response.json({ error: "unauthenticated", message: "MCP requests require a user" }, { status: 401 });
|
|
54
|
+
}
|
|
55
|
+
const versionHeader = request.headers.get("mcp-protocol-version");
|
|
56
|
+
if (versionHeader &&
|
|
57
|
+
!SUPPORTED_PROTOCOL_VERSIONS.includes(versionHeader)) {
|
|
58
|
+
return Response.json({
|
|
59
|
+
error: "unsupported_protocol_version",
|
|
60
|
+
supported: SUPPORTED_PROTOCOL_VERSIONS,
|
|
61
|
+
}, { status: 400 });
|
|
62
|
+
}
|
|
63
|
+
let message;
|
|
64
|
+
try {
|
|
65
|
+
message = (await request.json());
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return jsonRpcError(null, -32700, "parse error: body must be JSON");
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(message)) {
|
|
71
|
+
return jsonRpcError(null, -32600, "batch requests are not supported");
|
|
72
|
+
}
|
|
73
|
+
if (message === null || typeof message !== "object") {
|
|
74
|
+
return jsonRpcError(null, -32600, "invalid JSON-RPC message");
|
|
75
|
+
}
|
|
76
|
+
if (message.jsonrpc !== "2.0") {
|
|
77
|
+
return jsonRpcError(null, -32600, 'jsonrpc must be "2.0"');
|
|
78
|
+
}
|
|
79
|
+
if ("id" in message &&
|
|
80
|
+
typeof message.id !== "string" &&
|
|
81
|
+
typeof message.id !== "number" &&
|
|
82
|
+
message.id !== null) {
|
|
83
|
+
return jsonRpcError(null, -32600, "id must be a string, number, or null");
|
|
84
|
+
}
|
|
85
|
+
// Notifications and client-side responses expect no reply body.
|
|
86
|
+
if (typeof message.method !== "string")
|
|
87
|
+
return accepted();
|
|
88
|
+
if (!("id" in message))
|
|
89
|
+
return accepted();
|
|
90
|
+
const id = message.id;
|
|
91
|
+
switch (message.method) {
|
|
92
|
+
case "initialize":
|
|
93
|
+
return jsonRpcResult(id, {
|
|
94
|
+
protocolVersion: negotiateProtocolVersion(message.params),
|
|
95
|
+
capabilities: { tools: { listChanged: false } },
|
|
96
|
+
serverInfo: { name, version },
|
|
97
|
+
...(instructions ? { instructions } : {}),
|
|
98
|
+
});
|
|
99
|
+
case "ping":
|
|
100
|
+
return jsonRpcResult(id, {});
|
|
101
|
+
case "tools/list": {
|
|
102
|
+
const visible = Object.entries(tools)
|
|
103
|
+
.filter(([, tool]) => userHasScope(resolvedUser, tool.scope))
|
|
104
|
+
.map(([toolName, tool]) => ({
|
|
105
|
+
name: toolName,
|
|
106
|
+
...(tool.description ? { description: tool.description } : {}),
|
|
107
|
+
inputSchema: tool.inputSchema ?? { type: "object", properties: {} },
|
|
108
|
+
}));
|
|
109
|
+
return jsonRpcResult(id, { tools: visible });
|
|
110
|
+
}
|
|
111
|
+
case "tools/call": {
|
|
112
|
+
const params = message.params && typeof message.params === "object"
|
|
113
|
+
? message.params
|
|
114
|
+
: {};
|
|
115
|
+
const toolName = typeof params.name === "string" ? params.name : "";
|
|
116
|
+
const tool = tools[toolName];
|
|
117
|
+
if (!tool) {
|
|
118
|
+
return jsonRpcError(id, -32602, `unknown tool: ${toolName}`);
|
|
119
|
+
}
|
|
120
|
+
if (!userHasScope(resolvedUser, tool.scope)) {
|
|
121
|
+
return jsonRpcError(id, -32602, `tool ${toolName} requires scope ${tool.scope}`);
|
|
122
|
+
}
|
|
123
|
+
const args = params.arguments && typeof params.arguments === "object"
|
|
124
|
+
? params.arguments
|
|
125
|
+
: {};
|
|
126
|
+
try {
|
|
127
|
+
const value = await tool.handler(args, {
|
|
128
|
+
user: resolvedUser,
|
|
129
|
+
env,
|
|
130
|
+
request,
|
|
131
|
+
ctx,
|
|
132
|
+
});
|
|
133
|
+
return jsonRpcResult(id, toCallToolResult(value));
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
137
|
+
return jsonRpcResult(id, {
|
|
138
|
+
content: [{ type: "text", text }],
|
|
139
|
+
isError: true,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
default:
|
|
144
|
+
return jsonRpcError(id, -32601, `method not found: ${message.method}`);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -1,67 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@every-app/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
|
-
"./
|
|
8
|
-
"types": "./dist/
|
|
9
|
-
"default": "./dist/
|
|
7
|
+
"./server": {
|
|
8
|
+
"types": "./dist/server/index.d.ts",
|
|
9
|
+
"default": "./dist/server/index.js"
|
|
10
10
|
},
|
|
11
|
-
"./
|
|
12
|
-
"types": "./dist/
|
|
13
|
-
"default": "./dist/
|
|
14
|
-
},
|
|
15
|
-
"./tanstack/server": {
|
|
16
|
-
"types": "./dist/tanstack/server/index.d.ts",
|
|
17
|
-
"default": "./dist/tanstack/server/index.js"
|
|
18
|
-
},
|
|
19
|
-
"./cloudflare": {
|
|
20
|
-
"types": "./dist/cloudflare/index.d.ts",
|
|
21
|
-
"default": "./dist/cloudflare/index.js"
|
|
22
|
-
},
|
|
23
|
-
"./cloudflare/server": {
|
|
24
|
-
"types": "./dist/cloudflare/server/index.d.ts",
|
|
25
|
-
"default": "./dist/cloudflare/server/index.js"
|
|
11
|
+
"./internal": {
|
|
12
|
+
"types": "./dist/internal/index.d.ts",
|
|
13
|
+
"default": "./dist/internal/index.js"
|
|
26
14
|
}
|
|
27
15
|
},
|
|
28
16
|
"files": [
|
|
29
17
|
"dist",
|
|
30
18
|
"README.md"
|
|
31
19
|
],
|
|
32
|
-
"scripts": {
|
|
33
|
-
"build": "tsc -p tsconfig.build.json",
|
|
34
|
-
"prepack": "npm run build",
|
|
35
|
-
"build:prepublish": "pnpm install --ignore-scripts && npm run types:check && npm run build",
|
|
36
|
-
"prepublishOnly": "npm run build:prepublish",
|
|
37
|
-
"types:check": "tsc --noEmit",
|
|
38
|
-
"format:check": "prettier --check .",
|
|
39
|
-
"format:write": "prettier . --write",
|
|
40
|
-
"test": "vitest run --silent",
|
|
41
|
-
"test:watch": "vitest",
|
|
42
|
-
"check:esm-relative-imports": "node ./scripts/check-esm-relative-imports.mjs"
|
|
43
|
-
},
|
|
44
|
-
"dependencies": {
|
|
45
|
-
"jsonc-parser": "^3.3.1"
|
|
46
|
-
},
|
|
47
20
|
"peerDependencies": {
|
|
48
|
-
"
|
|
49
|
-
"@tanstack/react-start": "^1.0.0",
|
|
50
|
-
"jose": "^6.0.0",
|
|
51
|
-
"react": "^18.0.0 || ^19.0.0"
|
|
21
|
+
"jose": "^6.0.0"
|
|
52
22
|
},
|
|
53
23
|
"devDependencies": {
|
|
54
24
|
"@cloudflare/workers-types": "^4.20251014.0",
|
|
55
|
-
"@tanstack/react-router": "^1.136.3",
|
|
56
|
-
"@tanstack/react-start": "^1.136.3",
|
|
57
25
|
"@types/node": "^22.18.13",
|
|
58
|
-
"@types/react": "^19.0.8",
|
|
59
26
|
"jose": "^6.0.12",
|
|
60
27
|
"prettier": "^3.6.2",
|
|
61
|
-
"react": "^19.0.0",
|
|
62
28
|
"typescript": "^5.9.3",
|
|
63
29
|
"vite": "^7.1.2",
|
|
64
30
|
"vite-tsconfig-paths": "^5.1.4",
|
|
65
31
|
"vitest": "^3.2.4"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"prebuild": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
35
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
|
+
"build:prepublish": "pnpm install --ignore-scripts && npm run types:check && npm run build",
|
|
37
|
+
"types:check": "tsc --noEmit",
|
|
38
|
+
"format:check": "prettier --check .",
|
|
39
|
+
"format:write": "prettier . --write",
|
|
40
|
+
"test": "vitest run --silent",
|
|
41
|
+
"test:watch": "vitest",
|
|
42
|
+
"check:esm-relative-imports": "node ./scripts/check-esm-relative-imports.mjs"
|
|
66
43
|
}
|
|
67
|
-
}
|
|
44
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"getLocalD1Url.d.ts","sourceRoot":"","sources":["../../src/cloudflare/getLocalD1Url.ts"],"names":[],"mappings":"AAWA,wBAAgB,aAAa,kBAoD5B"}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import { execSync } from "child_process";
|
|
4
|
-
import { parse } from "jsonc-parser";
|
|
5
|
-
function findSqliteFile(basePath) {
|
|
6
|
-
return fs
|
|
7
|
-
.readdirSync(basePath, { encoding: "utf-8", recursive: true })
|
|
8
|
-
.find((f) => f.endsWith(".sqlite"));
|
|
9
|
-
}
|
|
10
|
-
export function getLocalD1Url() {
|
|
11
|
-
const basePath = path.resolve(".wrangler");
|
|
12
|
-
// Check if .wrangler directory exists
|
|
13
|
-
if (!fs.existsSync(basePath)) {
|
|
14
|
-
console.error("================================================================================");
|
|
15
|
-
console.error("WARNING: .wrangler directory not found");
|
|
16
|
-
console.error("This is expected in CI/non-development environments.");
|
|
17
|
-
console.error("The local D1 database is only available after running 'wrangler dev' which you can trigger by running 'npm run dev'.");
|
|
18
|
-
console.error("================================================================================");
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
let dbFile = findSqliteFile(basePath);
|
|
22
|
-
if (!dbFile) {
|
|
23
|
-
// Read wrangler.jsonc to get the database name
|
|
24
|
-
const wranglerConfigPath = path.resolve("wrangler.jsonc");
|
|
25
|
-
const wranglerConfig = parse(fs.readFileSync(wranglerConfigPath, "utf-8"));
|
|
26
|
-
const databaseName = wranglerConfig.d1_databases?.[0]?.database_name;
|
|
27
|
-
if (!databaseName) {
|
|
28
|
-
throw new Error("Could not find database_name in wrangler.jsonc d1_databases configuration");
|
|
29
|
-
}
|
|
30
|
-
// Execute the command to initialize the local database
|
|
31
|
-
console.log(`Initializing local D1 database: ${databaseName}...`);
|
|
32
|
-
execSync(`npx wrangler d1 execute ${databaseName} --local --command "SELECT 1;"`, { stdio: "pipe" });
|
|
33
|
-
// Try to find the db file again after initialization
|
|
34
|
-
dbFile = findSqliteFile(basePath);
|
|
35
|
-
if (!dbFile) {
|
|
36
|
-
throw new Error(`Failed to initialize local D1 database. The sqlite file was not created.`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return path.resolve(basePath, dbFile);
|
|
40
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cloudflare/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/cloudflare/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { lazyInitForWorkers } from "./lazyInit.js";
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wraps a factory function in a Proxy to defer initialization until first access.
|
|
3
|
-
* This prevents async operations (Like creating Tanstack DB Collections) from running in Cloudflare Workers' global scope.
|
|
4
|
-
*
|
|
5
|
-
* @param factory - A function that creates and returns the resource.
|
|
6
|
-
* Must be a callback to defer execution; passing the value directly
|
|
7
|
-
* would evaluate it at module load time, triggering the Cloudflare error.
|
|
8
|
-
* @returns A Proxy that lazily initializes the resource on first property access
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* export const myCollection = lazyInitForWorkers(() =>
|
|
13
|
-
* createCollection(queryCollectionOptions({
|
|
14
|
-
* queryKey: ["myData"],
|
|
15
|
-
* queryFn: async () => fetchData(),
|
|
16
|
-
* // ... other options
|
|
17
|
-
* }))
|
|
18
|
-
* );
|
|
19
|
-
* ```
|
|
20
|
-
*/
|
|
21
|
-
export declare function lazyInitForWorkers<T extends object>(factory: () => T): T;
|
|
22
|
-
//# sourceMappingURL=lazyInit.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"lazyInit.d.ts","sourceRoot":"","sources":["../../src/cloudflare/lazyInit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,CA8CxE"}
|