@shivaedev/mcp 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShivaeDev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,135 @@
1
- # Temporary Holding Version
1
+ # @shivaedev/mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Let AI clients act on application data as the user who connected them. Keep tools in native Effect toolkits, mount them on your application host, and use the same identity and permissions as your contracts.
4
+
5
+ ## Tools for each audience
6
+
7
+ A signed-in user discovers user tools. An administrator also discovers admin tools. A plain user cannot call a hidden admin tool by guessing its name.
8
+
9
+ ```ts
10
+ const routes = mcpRoutes({
11
+ name: "My app",
12
+ version: "1",
13
+ path: "/mcp",
14
+ sessions: oauth.sessions,
15
+ resourceMetadataUrl: oauth.resourceMetadataUrl,
16
+ audiences: [{ tools: userTools }, { permission: ManageTools.any, tools: adminTools }],
17
+ });
18
+ ```
19
+
20
+ ## Using it
21
+
22
+ Each request authenticates through identity's session policy. The route registers only the caller's permitted audiences with Effect's `McpServer`; discovery and dispatch use that same registry. Handlers receive `Identity` and `SessionUser` from the current request. A shared application Layer never retains a caller's identity.
23
+
24
+ ### Declare native tools and handlers
25
+
26
+ Use `Tool` and `Toolkit` from `effect/ai`. Declare handler dependencies and failure schemas on the tool, just as contracts declare dependencies and typed failures. Platform does not add another tool definition or handler API.
27
+
28
+ ```ts
29
+ import { Effect, Layer, Schema } from "effect";
30
+ import { McpServer, Tool, Toolkit } from "effect/ai";
31
+ import { permission } from "@shivaedev/access/permission.ts";
32
+ import { role } from "@shivaedev/access/role.ts";
33
+ import { roleSet } from "@shivaedev/access/roleSet.ts";
34
+ import { Identity } from "@shivaedev/identity/identity.ts";
35
+
36
+ const ManageTools = permission("admin.tools");
37
+ const Roles = roleSet(role("user", []), role("admin", [ManageTools.any]));
38
+ const UserTools = Toolkit.make(
39
+ Tool.make("whoami", {
40
+ success: Schema.String,
41
+ dependencies: [Identity],
42
+ }),
43
+ );
44
+ const AdminTools = Toolkit.make(
45
+ Tool.make("adminIdentity", {
46
+ success: Schema.String,
47
+ dependencies: [Identity],
48
+ }),
49
+ );
50
+ const userTools = McpServer.toolkit(UserTools).pipe(
51
+ Layer.provide(
52
+ UserTools.toLayer({
53
+ whoami: () => Effect.map(Identity, ({ id }) => id),
54
+ }),
55
+ ),
56
+ );
57
+ const adminTools = McpServer.toolkit(AdminTools).pipe(
58
+ Layer.provide(
59
+ AdminTools.toLayer({
60
+ adminIdentity: () => Effect.map(Identity, ({ id }) => id),
61
+ }),
62
+ ),
63
+ );
64
+ ```
65
+
66
+ Application roles and grants come from `@shivaedev/access`; identity resolves the current stored role and checks those grants. An audience without a permission is available to every signed-in caller. Add `failure: YourErrorSchema` to a tool and return that failure in its Effect handler; native MCP error results carry the declared failure.
67
+
68
+ ### Connect OAuth to identity and the host
69
+
70
+ The Better Auth adapter is an optional edge module. Add its provider to the auth instance used by identity, alongside Better Auth's JWT plugin. `mcpProvider` configures the `mcp` scope and the endpoint's OAuth resource. The application's login and consent pages implement the provider's ordinary UI flow; the provider owns authorization codes, PKCE and tokens.
71
+
72
+ ```ts
73
+ import { admin, jwt } from "better-auth/plugins";
74
+ import { Effect, Layer } from "effect";
75
+ import { betterAuthMount } from "@shivaedev/identity/server/better-auth/mount.ts";
76
+ import { trustedOrigins } from "@shivaedev/identity/server/origin.ts";
77
+ import { betterAuthMcp, mcpProvider } from "@shivaedev/mcp/adapters/betterAuth.ts";
78
+ import { mcpRoutes } from "@shivaedev/mcp/routes.ts";
79
+ import { appHost } from "@shivaedev/server/appHost.ts";
80
+
81
+ const application = Effect.gen(function* () {
82
+ const resource = "https://app.example.com/mcp";
83
+ const mounted = yield* betterAuthMount(
84
+ {
85
+ baseURL: "https://app.example.com",
86
+ database: authPool,
87
+ plugins: [
88
+ jwt(),
89
+ mcpProvider({
90
+ resource,
91
+ loginPage: "/login",
92
+ consentPage: "/consent",
93
+ allowDynamicClientRegistration: true,
94
+ allowUnauthenticatedClientRegistration: true,
95
+ }),
96
+ ],
97
+ },
98
+ { roles: Roles },
99
+ );
100
+ const origin = trustedOrigins({ allow: ["https://app.example.com"], missing: "allow" });
101
+ const oauth = yield* betterAuthMcp({ auth: mounted.auth, resource, origin, roles: Roles });
102
+ return appHost({
103
+ group: Api,
104
+ handlers: Handlers,
105
+ sessions: { ...mounted.sessions, origin },
106
+ routes: Layer.mergeAll(
107
+ mounted.routes,
108
+ oauth.routes,
109
+ mcpRoutes({
110
+ name: "My app",
111
+ version: "1",
112
+ path: "/mcp",
113
+ sessions: oauth.sessions,
114
+ resourceMetadataUrl: oauth.resourceMetadataUrl,
115
+ audiences: [{ tools: userTools }, { permission: ManageTools.any, tools: adminTools }],
116
+ }),
117
+ ),
118
+ });
119
+ });
120
+ ```
121
+
122
+ `authPool`, `Api` and `Handlers` are the application's existing auth database, RPC group and handlers. Run the Better Auth migrations with the OAuth and JWT plugins included. For existing role management, keep the application's existing role plugin and `RoleSet` in this configuration.
123
+
124
+ The edge adapter verifies access-token signatures, issuer, endpoint audience and `mcp` scope, then loads the current user through Better Auth. Identity resolves the stored role on each request. An invalid token receives `401` with a `WWW-Authenticate` protected-resource metadata pointer. The metadata names the authorization server and its discovery endpoint, so a client can start OAuth. MCP bearer credentials are separate from browser session cookies.
125
+
126
+ ### Install and limits
127
+
128
+ ```sh
129
+ pnpm add @shivaedev/mcp effect
130
+ pnpm add better-auth @better-auth/oauth-provider
131
+ ```
132
+
133
+ The OAuth dependencies are optional peers, imported only by `adapters/betterAuth.ts`. Core `routes.ts` also accepts another identity `SessionPolicy` with the same identity and permission semantics.
134
+
135
+ The endpoint uses Effect's request-scoped Streamable HTTP transport and MCP `2026-07-28`. Clients must support that protocol; earlier session-based versions and subscription streams are outside this adapter. Endpoint requests do not share registries, identities or MCP sessions.
@@ -0,0 +1,35 @@
1
+ import { type OAuthOptions, oauthProvider, type Scope } from "@better-auth/oauth-provider";
2
+ import type { AuthContext, BetterAuthOptions } from "better-auth";
3
+ import { Effect } from "effect";
4
+ import { HttpRouter } from "effect/http";
5
+ import type { RoleSet } from "@shivaedev/access/roleSet.ts";
6
+ import { AuthUnavailable } from "@shivaedev/errors/taxonomy.ts";
7
+ import type { OriginPolicy } from "@shivaedev/identity/server/origin.ts";
8
+ import type { SessionPolicy, UserSessionShape } from "@shivaedev/identity/server/session.ts";
9
+ export declare function mcpProvider({ resource, ...options }: Omit<OAuthOptions<Scope[]>, "resources" | "scopes" | "clientRegistrationDefaultResources"> & {
10
+ readonly resource: string;
11
+ }): ReturnType<typeof oauthProvider>;
12
+ interface OauthAuth {
13
+ readonly $context: Promise<{
14
+ readonly internalAdapter: Pick<AuthContext["internalAdapter"], "findUserById">;
15
+ }>;
16
+ readonly api: {
17
+ readonly "getOAuthServerConfig": () => Promise<{
18
+ readonly issuer: string;
19
+ readonly "jwks_uri"?: string;
20
+ }>;
21
+ };
22
+ readonly options: BetterAuthOptions;
23
+ }
24
+ export declare const betterAuthMcp: (options: {
25
+ readonly auth: OauthAuth;
26
+ readonly resource: string;
27
+ readonly origin: OriginPolicy;
28
+ readonly roles?: RoleSet;
29
+ }) => Effect.Effect<{
30
+ resourceMetadataUrl: string;
31
+ routes: import("effect/Layer").Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", import("effect/http/HttpServerError").InternalError> | HttpRouter.Request<"Error", import("effect/http/HttpServerError").RequestParseError> | HttpRouter.Request<"Error", import("effect/http/HttpServerError").RouteNotFound>>;
32
+ sessions: SessionPolicy<UserSessionShape>;
33
+ }, AuthUnavailable, never>;
34
+ export {};
35
+ //# sourceMappingURL=betterAuth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"betterAuth.d.ts","sourceRoot":"","sources":["../../src/adapters/betterAuth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAE,aAAa,EAAmC,KAAK,KAAK,EAAE,MAAM,6BAA6B,CAAC;AAE5H,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,UAAU,EAAyC,MAAM,aAAa,CAAC;AAChF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,8BAA8B,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAE7F,wBAAgB,WAAW,CAAC,EAC3B,QAAQ,EACR,GAAG,OAAO,EACV,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC,EAAE,WAAW,GAAG,QAAQ,GAAG,oCAAoC,CAAC,GAAG;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CACzI,OAAO,aAAa,CACpB,CAEA;AAED,UAAU,SAAS;IAClB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;QAAE,QAAQ,CAAC,eAAe,EAAE,IAAI,CAAC,WAAW,CAAC,iBAAiB,CAAC,EAAE,cAAc,CAAC,CAAA;KAAE,CAAC,CAAC;IAC/G,QAAQ,CAAC,GAAG,EAAE;QAAE,QAAQ,CAAC,sBAAsB,EAAE,MAAM,OAAO,CAAC;YAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;YAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC;IAC5H,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAC;CACpC;AAED,eAAO,MAAM,aAAa;mBACV,SAAS;uBACL,MAAM;qBACR,YAAY;qBACZ,OAAO;;;;;0BA0DvB,CAAC"}
@@ -0,0 +1,58 @@
1
+ import { oauthProvider, oauthProviderAuthServerMetadata } from "@better-auth/oauth-provider";
2
+ import { oauthProviderResourceClient } from "@better-auth/oauth-provider/resource-client";
3
+ import { Effect, Option } from "effect";
4
+ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
5
+ import { AuthUnavailable } from "@shivaedev/errors/taxonomy.ts";
6
+ export function mcpProvider({ resource, ...options }) {
7
+ return oauthProvider({ ...options, clientRegistrationDefaultResources: [resource], resources: [resource], scopes: ["mcp"] });
8
+ }
9
+ export const betterAuthMcp = Effect.fn("Mcp.betterAuthMcp")(function* (options) {
10
+ const config = yield* Effect.tryPromise({
11
+ catch: () => new AuthUnavailable({ message: "OAuth provider configuration failed" }),
12
+ try: () => options.auth.api.getOAuthServerConfig(),
13
+ });
14
+ const { auth, resource } = options;
15
+ const url = new URL(resource);
16
+ const metadataPath = `/.well-known/oauth-protected-resource${url.pathname}`;
17
+ const resourceMetadataUrl = new URL(metadataPath, url).href;
18
+ const client = oauthProviderResourceClient(auth).getActions();
19
+ const sessions = {
20
+ origin: options.origin,
21
+ ...(options.roles === undefined ? {} : { roles: options.roles }),
22
+ provider: {
23
+ get: (headers) => Effect.gen(function* () {
24
+ const authorization = headers.authorization;
25
+ const token = authorization?.startsWith("Bearer ") ? authorization.slice(7) : undefined;
26
+ const claims = yield* Effect.tryPromise(() => client.verifyBearerToken(token, {
27
+ jwksUrl: config.jwks_uri ?? new URL("/api/auth/jwks", url).href,
28
+ requiredScopes: ["mcp"],
29
+ verifyOptions: { audience: resource, issuer: config.issuer },
30
+ })).pipe(Effect.option);
31
+ if (Option.isNone(claims) || typeof claims.value.sub !== "string") {
32
+ return Option.none();
33
+ }
34
+ const id = claims.value.sub;
35
+ const user = yield* Effect.tryPromise({
36
+ catch: () => new AuthUnavailable({ message: "OAuth user lookup failed" }),
37
+ try: async () => (await auth.$context).internalAdapter.findUserById(id),
38
+ });
39
+ if (user === null) {
40
+ return Option.none();
41
+ }
42
+ const role = Reflect.get(user, "role");
43
+ return Option.some({ user: { email: user.email, id: user.id, name: user.name, ...(typeof role === "string" ? { role } : {}) } });
44
+ }),
45
+ },
46
+ };
47
+ const metadata = oauthProviderAuthServerMetadata(auth);
48
+ const authorizationMetadata = Effect.fn("Mcp.authorizationMetadata")(function* (request) {
49
+ const web = yield* HttpServerRequest.toWeb(request);
50
+ return HttpServerResponse.fromWeb(yield* Effect.promise(() => metadata(web)));
51
+ });
52
+ const routes = HttpRouter.use((router) => Effect.gen(function* () {
53
+ yield* router.add("GET", metadataPath, HttpServerResponse.jsonUnsafe({ "authorization_servers": [config.issuer], resource, "scopes_supported": ["mcp"] }));
54
+ yield* router.add("GET", "/.well-known/oauth-authorization-server/api/auth", authorizationMetadata);
55
+ }));
56
+ return { resourceMetadataUrl, routes, sessions };
57
+ });
58
+ //# sourceMappingURL=betterAuth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"betterAuth.js","sourceRoot":"","sources":["../../src/adapters/betterAuth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import { Effect } from "effect";
2
+ import { HttpRouter } from "effect/http";
3
+ import { SessionUser } from "@shivaedev/identity/identity.ts";
4
+ import type { UserSessionShape } from "@shivaedev/identity/server/session.ts";
5
+ import type { McpOptions } from "./routes.ts";
6
+ export declare const requestServer: <TSession extends UserSessionShape, E, R>(options: McpOptions<TSession, E, R>) => Effect.Effect<import("effect/http/HttpServerResponse").HttpServerResponse, unknown, import("effect/http/HttpServerRequest").HttpServerRequest | import("effect/Scope").Scope | SessionUser | Exclude<R, HttpRouter.HttpRouter>>;
7
+ //# sourceMappingURL=requestServer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requestServer.d.ts","sourceRoot":"","sources":["../src/requestServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAS,MAAM,QAAQ,CAAC;AAEvC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAE9D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uCAAuC,CAAC;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,eAAO,MAAM,aAAa,GAA6C,QAAQ,SAAS,gBAAgB,EAAE,CAAC,EAAE,CAAC,yQAS5G,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { McpProtocol, McpServer } from "effect/ai";
3
+ import { HttpRouter } from "effect/http";
4
+ import { SessionUser } from "@shivaedev/identity/identity.ts";
5
+ import { hasPermission } from "@shivaedev/identity/permission.ts";
6
+ export const requestServer = Effect.fn("Mcp.requestServer")(function* (options) {
7
+ const user = yield* SessionUser;
8
+ const inner = yield* HttpRouter.make;
9
+ const transport = McpServer.layerHttpRequestScoped({ ...options, protocols: [McpProtocol.v2026_07_28] });
10
+ const tools = options.audiences
11
+ .filter(({ permission }) => permission === undefined || hasPermission(user, permission))
12
+ .map((audience) => audience.tools);
13
+ yield* Layer.build(Layer.mergeAll(transport, ...tools)).pipe(Effect.provideService(HttpRouter.HttpRouter, inner));
14
+ return yield* inner.asHttpEffect();
15
+ });
16
+ //# sourceMappingURL=requestServer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"requestServer.js","sourceRoot":"","sources":["../src/requestServer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAIlE,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,EAAE,CAAC,mBAAmB,CAAC,CAAC,QAAQ,CAAC,EAA2C,OAAmC;IAClJ,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,WAAW,CAAC;IAChC,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC;IACrC,MAAM,SAAS,GAAG,SAAS,CAAC,sBAAsB,CAAC,EAAE,GAAG,OAAO,EAAE,SAAS,EAAE,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IACzG,MAAM,KAAK,GAAG,OAAO,CAAC,SAAS;SAC7B,MAAM,CAAC,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,UAAU,KAAK,SAAS,IAAI,aAAa,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;SACvF,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpC,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,UAAU,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC;IAClH,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,YAAY,EAAE,CAAC;AACpC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { type Layer } from "effect";
2
+ import { HttpRouter } from "effect/http";
3
+ import type { Permission } from "@shivaedev/access/permission.ts";
4
+ import type { Identity, SessionUser } from "@shivaedev/identity/identity.ts";
5
+ import { type SessionPolicy, type UserSessionShape } from "@shivaedev/identity/server/session.ts";
6
+ export interface Audience<E = never, R = never> {
7
+ readonly permission?: Permission;
8
+ readonly tools: Layer.Layer<never, E, R>;
9
+ }
10
+ export interface McpOptions<TSession extends UserSessionShape, E, R> {
11
+ readonly allowedOrigins?: readonly string[];
12
+ readonly audiences: readonly Audience<E, R>[];
13
+ readonly name: string;
14
+ readonly path: HttpRouter.PathInput;
15
+ readonly resourceMetadataUrl: string;
16
+ readonly sessions: SessionPolicy<TSession>;
17
+ readonly version: string;
18
+ }
19
+ export declare function mcpRoutes<TSession extends UserSessionShape, E, R>(options: McpOptions<TSession, E, R>): Layer.Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", unknown> | Exclude<HttpRouter.Request.From<"Requires", Exclude<Exclude<Exclude<R, HttpRouter.HttpRouter>, Identity | SessionUser>, HttpRouter.Provided>>, import("effect/Scope").Scope>>;
20
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,KAAK,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAsB,MAAM,aAAa,CAAC;AAE7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,iCAAiC,CAAC;AAC7E,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,gBAAgB,EAAmB,MAAM,uCAAuC,CAAC;AAGnH,MAAM,WAAW,QAAQ,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC,GAAG,KAAK;IAC7C,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IACjC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,UAAU,CAAC,QAAQ,SAAS,gBAAgB,EAAE,CAAC,EAAE,CAAC;IAClE,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,QAAQ,CAAC,SAAS,EAAE,SAAS,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC;IACpC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAaD,wBAAgB,SAAS,CAAC,QAAQ,SAAS,gBAAgB,EAAE,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC,0QAarG"}
package/dist/routes.js ADDED
@@ -0,0 +1,23 @@
1
+ import { Effect } from "effect";
2
+ import { HttpRouter, HttpServerResponse } from "effect/http";
3
+ import * as HttpStatus from "effect/http/HttpStatus";
4
+ import { withSessionUser } from "@shivaedev/identity/server/session.ts";
5
+ import { requestServer } from "./requestServer.js";
6
+ function rejection(error, metadataUrl) {
7
+ return HttpServerResponse.empty({
8
+ headers: error._tag === "Unauthorized" ? { "cache-control": "no-store", "www-authenticate": `Bearer resource_metadata="${metadataUrl}"` } : {},
9
+ status: {
10
+ AuthUnavailable: HttpStatus.fromLiteral("ServiceUnavailable"),
11
+ Forbidden: HttpStatus.fromLiteral("Forbidden"),
12
+ Unauthorized: HttpStatus.fromLiteral("Unauthorized"),
13
+ }[error._tag],
14
+ });
15
+ }
16
+ export function mcpRoutes(options) {
17
+ const server = requestServer(options);
18
+ return HttpRouter.use((router) => router.add("*", options.path, (request) => Effect.context().pipe(withSessionUser(options.sessions, request.headers, request.url), Effect.matchEffect({
19
+ onFailure: (error) => Effect.succeed(rejection(error, options.resourceMetadataUrl)),
20
+ onSuccess: (context) => Effect.provideContext(server, context),
21
+ }))));
22
+ }
23
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAc,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,KAAK,UAAU,MAAM,wBAAwB,CAAC;AAIrD,OAAO,EAA6C,eAAe,EAAE,MAAM,uCAAuC,CAAC;AACnH,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAiBnD,SAAS,SAAS,CAAC,KAAiD,EAAE,WAAmB;IACxF,OAAO,kBAAkB,CAAC,KAAK,CAAC;QAC/B,OAAO,EAAE,KAAK,CAAC,IAAI,KAAK,cAAc,CAAC,CAAC,CAAC,EAAE,eAAe,EAAE,UAAU,EAAE,kBAAkB,EAAE,6BAA6B,WAAW,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE;QAC9I,MAAM,EAAE;YACP,eAAe,EAAE,UAAU,CAAC,WAAW,CAAC,oBAAoB,CAAC;YAC7D,SAAS,EAAE,UAAU,CAAC,WAAW,CAAC,WAAW,CAAC;YAC9C,YAAY,EAAE,UAAU,CAAC,WAAW,CAAC,cAAc,CAAC;SACpD,CAAC,KAAK,CAAC,IAAI,CAAC;KACb,CAAC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,SAAS,CAA0C,OAAmC;IACrG,MAAM,MAAM,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACtC,OAAO,UAAU,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAChC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE,CACzC,MAAM,CAAC,OAAO,EAA0B,CAAC,IAAI,CAC5C,eAAe,CAAC,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,EAC/D,MAAM,CAAC,WAAW,CAAC;QAClB,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,KAAK,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC;QACnF,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,OAAO,CAAC;KAC9D,CAAC,CACF,CACD,CACD,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,93 @@
1
1
  {
2
2
  "name": "@shivaedev/mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Identity-aware Effect MCP tools mounted on the application host",
5
+ "keywords": [
6
+ "effect"
7
+ ],
8
+ "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/mcp#readme",
9
+ "bugs": {
10
+ "url": "https://github.com/ShivaeDev/platform/issues"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/ShivaeDev/platform.git",
15
+ "directory": "packages/mcp"
16
+ },
17
+ "license": "MIT",
18
+ "sideEffects": false,
19
+ "type": "module",
20
+ "imports": {
21
+ "#test/*": "./src/test-support/*",
22
+ "#*.ts": {
23
+ "source": "./src/*.ts",
24
+ "types": "./dist/*.d.ts",
25
+ "default": "./dist/*.js"
26
+ }
27
+ },
28
+ "exports": {
29
+ "./*.ts": {
30
+ "source": "./src/*.ts",
31
+ "types": "./dist/*.d.ts",
32
+ "default": "./dist/*.js"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "src",
39
+ "!src/**/*.test.ts",
40
+ "!src/**/*.spec.ts",
41
+ "!src/**/test-support",
42
+ "README.md"
43
+ ],
44
+ "dependencies": {
45
+ "@shivaedev/access": "0.1.2",
46
+ "@shivaedev/errors": "0.1.1",
47
+ "@shivaedev/identity": "0.4.0"
48
+ },
49
+ "devDependencies": {
50
+ "@better-auth/oauth-provider": "1.7.12",
51
+ "@effect/platform-node": "4.0.6",
52
+ "@effect/sql-pg": "4.0.6",
53
+ "@shivaedev/quality": "0.45.0",
54
+ "@shivaedev/server": "0.5.0",
55
+ "@shivaedev/test-sql": "0.11.0",
56
+ "@shivaedev/test-story": "0.24.0",
57
+ "@types/node": "24.19.2",
58
+ "better-auth": "1.7.12",
59
+ "effect": "4.0.6",
60
+ "pg": "8.23.0",
61
+ "typescript": "7.0.2",
62
+ "vitest": "5.0.5"
63
+ },
64
+ "peerDependencies": {
65
+ "@better-auth/oauth-provider": "1.7.12",
66
+ "better-auth": "1.7.12",
67
+ "effect": "4.0.6"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "@better-auth/oauth-provider": {
71
+ "optional": true
72
+ },
73
+ "better-auth": {
74
+ "optional": true
75
+ }
76
+ },
77
+ "engines": {
78
+ "node": ">=24"
79
+ },
80
+ "publishConfig": {
81
+ "access": "public",
82
+ "provenance": true
83
+ },
84
+ "quality": {
85
+ "runtime": "server"
86
+ },
87
+ "scripts": {
88
+ "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
89
+ "ready": "pnpm typecheck && pnpm test && pnpm build",
90
+ "test": "vitest run",
91
+ "typecheck": "tsc --noEmit"
92
+ }
6
93
  }
@@ -0,0 +1,88 @@
1
+ import { type OAuthOptions, oauthProvider, oauthProviderAuthServerMetadata, type Scope } from "@better-auth/oauth-provider";
2
+ import { oauthProviderResourceClient } from "@better-auth/oauth-provider/resource-client";
3
+ import type { AuthContext, BetterAuthOptions } from "better-auth";
4
+ import { Effect, Option } from "effect";
5
+ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
6
+ import type { RoleSet } from "@shivaedev/access/roleSet.ts";
7
+ import { AuthUnavailable } from "@shivaedev/errors/taxonomy.ts";
8
+ import type { OriginPolicy } from "@shivaedev/identity/server/origin.ts";
9
+ import type { SessionPolicy, UserSessionShape } from "@shivaedev/identity/server/session.ts";
10
+
11
+ export function mcpProvider({
12
+ resource,
13
+ ...options
14
+ }: Omit<OAuthOptions<Scope[]>, "resources" | "scopes" | "clientRegistrationDefaultResources"> & { readonly resource: string }): ReturnType<
15
+ typeof oauthProvider
16
+ > {
17
+ return oauthProvider({ ...options, clientRegistrationDefaultResources: [resource], resources: [resource], scopes: ["mcp"] });
18
+ }
19
+
20
+ interface OauthAuth {
21
+ readonly $context: Promise<{ readonly internalAdapter: Pick<AuthContext["internalAdapter"], "findUserById"> }>;
22
+ readonly api: { readonly "getOAuthServerConfig": () => Promise<{ readonly issuer: string; readonly "jwks_uri"?: string }> };
23
+ readonly options: BetterAuthOptions;
24
+ }
25
+
26
+ export const betterAuthMcp = Effect.fn("Mcp.betterAuthMcp")(function* (options: {
27
+ readonly auth: OauthAuth;
28
+ readonly resource: string;
29
+ readonly origin: OriginPolicy;
30
+ readonly roles?: RoleSet;
31
+ }) {
32
+ const config = yield* Effect.tryPromise({
33
+ catch: () => new AuthUnavailable({ message: "OAuth provider configuration failed" }),
34
+ try: () => options.auth.api.getOAuthServerConfig(),
35
+ });
36
+ const { auth, resource } = options;
37
+ const url = new URL(resource);
38
+ const metadataPath = `/.well-known/oauth-protected-resource${url.pathname}` as const;
39
+ const resourceMetadataUrl = new URL(metadataPath, url).href;
40
+ const client = oauthProviderResourceClient(auth).getActions();
41
+ const sessions: SessionPolicy<UserSessionShape> = {
42
+ origin: options.origin,
43
+ ...(options.roles === undefined ? {} : { roles: options.roles }),
44
+ provider: {
45
+ get: (headers) =>
46
+ Effect.gen(function* () {
47
+ const authorization = headers.authorization;
48
+ const token = authorization?.startsWith("Bearer ") ? authorization.slice(7) : undefined;
49
+ const claims = yield* Effect.tryPromise(() =>
50
+ client.verifyBearerToken(token, {
51
+ jwksUrl: config.jwks_uri ?? new URL("/api/auth/jwks", url).href,
52
+ requiredScopes: ["mcp"],
53
+ verifyOptions: { audience: resource, issuer: config.issuer },
54
+ }),
55
+ ).pipe(Effect.option);
56
+ if (Option.isNone(claims) || typeof claims.value.sub !== "string") {
57
+ return Option.none();
58
+ }
59
+ const id = claims.value.sub;
60
+ const user = yield* Effect.tryPromise({
61
+ catch: () => new AuthUnavailable({ message: "OAuth user lookup failed" }),
62
+ try: async () => (await auth.$context).internalAdapter.findUserById(id),
63
+ });
64
+ if (user === null) {
65
+ return Option.none();
66
+ }
67
+ const role: unknown = Reflect.get(user, "role");
68
+ return Option.some({ user: { email: user.email, id: user.id, name: user.name, ...(typeof role === "string" ? { role } : {}) } });
69
+ }),
70
+ },
71
+ };
72
+ const metadata = oauthProviderAuthServerMetadata(auth);
73
+ const authorizationMetadata = Effect.fn("Mcp.authorizationMetadata")(function* (request: HttpServerRequest.HttpServerRequest) {
74
+ const web = yield* HttpServerRequest.toWeb(request);
75
+ return HttpServerResponse.fromWeb(yield* Effect.promise(() => metadata(web)));
76
+ });
77
+ const routes = HttpRouter.use((router) =>
78
+ Effect.gen(function* () {
79
+ yield* router.add(
80
+ "GET",
81
+ metadataPath,
82
+ HttpServerResponse.jsonUnsafe({ "authorization_servers": [config.issuer], resource, "scopes_supported": ["mcp"] }),
83
+ );
84
+ yield* router.add("GET", "/.well-known/oauth-authorization-server/api/auth", authorizationMetadata);
85
+ }),
86
+ );
87
+ return { resourceMetadataUrl, routes, sessions };
88
+ });
@@ -0,0 +1,18 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { McpProtocol, McpServer } from "effect/ai";
3
+ import { HttpRouter } from "effect/http";
4
+ import { SessionUser } from "@shivaedev/identity/identity.ts";
5
+ import { hasPermission } from "@shivaedev/identity/permission.ts";
6
+ import type { UserSessionShape } from "@shivaedev/identity/server/session.ts";
7
+ import type { McpOptions } from "./routes.ts";
8
+
9
+ export const requestServer = Effect.fn("Mcp.requestServer")(function* <TSession extends UserSessionShape, E, R>(options: McpOptions<TSession, E, R>) {
10
+ const user = yield* SessionUser;
11
+ const inner = yield* HttpRouter.make;
12
+ const transport = McpServer.layerHttpRequestScoped({ ...options, protocols: [McpProtocol.v2026_07_28] });
13
+ const tools = options.audiences
14
+ .filter(({ permission }) => permission === undefined || hasPermission(user, permission))
15
+ .map((audience) => audience.tools);
16
+ yield* Layer.build(Layer.mergeAll(transport, ...tools)).pipe(Effect.provideService(HttpRouter.HttpRouter, inner));
17
+ return yield* inner.asHttpEffect();
18
+ });
package/src/routes.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { Effect, type Layer } from "effect";
2
+ import { HttpRouter, HttpServerResponse } from "effect/http";
3
+ import * as HttpStatus from "effect/http/HttpStatus";
4
+ import type { Permission } from "@shivaedev/access/permission.ts";
5
+ import type { AuthUnavailable, Forbidden, Unauthorized } from "@shivaedev/errors/taxonomy.ts";
6
+ import type { Identity, SessionUser } from "@shivaedev/identity/identity.ts";
7
+ import { type SessionPolicy, type UserSessionShape, withSessionUser } from "@shivaedev/identity/server/session.ts";
8
+ import { requestServer } from "./requestServer.ts";
9
+
10
+ export interface Audience<E = never, R = never> {
11
+ readonly permission?: Permission;
12
+ readonly tools: Layer.Layer<never, E, R>;
13
+ }
14
+
15
+ export interface McpOptions<TSession extends UserSessionShape, E, R> {
16
+ readonly allowedOrigins?: readonly string[];
17
+ readonly audiences: readonly Audience<E, R>[];
18
+ readonly name: string;
19
+ readonly path: HttpRouter.PathInput;
20
+ readonly resourceMetadataUrl: string;
21
+ readonly sessions: SessionPolicy<TSession>;
22
+ readonly version: string;
23
+ }
24
+
25
+ function rejection(error: AuthUnavailable | Forbidden | Unauthorized, metadataUrl: string) {
26
+ return HttpServerResponse.empty({
27
+ headers: error._tag === "Unauthorized" ? { "cache-control": "no-store", "www-authenticate": `Bearer resource_metadata="${metadataUrl}"` } : {},
28
+ status: {
29
+ AuthUnavailable: HttpStatus.fromLiteral("ServiceUnavailable"),
30
+ Forbidden: HttpStatus.fromLiteral("Forbidden"),
31
+ Unauthorized: HttpStatus.fromLiteral("Unauthorized"),
32
+ }[error._tag],
33
+ });
34
+ }
35
+
36
+ export function mcpRoutes<TSession extends UserSessionShape, E, R>(options: McpOptions<TSession, E, R>) {
37
+ const server = requestServer(options);
38
+ return HttpRouter.use((router) =>
39
+ router.add("*", options.path, (request) =>
40
+ Effect.context<Identity | SessionUser>().pipe(
41
+ withSessionUser(options.sessions, request.headers, request.url),
42
+ Effect.matchEffect({
43
+ onFailure: (error) => Effect.succeed(rejection(error, options.resourceMetadataUrl)),
44
+ onSuccess: (context) => Effect.provideContext(server, context),
45
+ }),
46
+ ),
47
+ ),
48
+ );
49
+ }