@protobase/server 0.41.0 → 0.41.1
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/admin-options.d.ts +3 -0
- package/dist/create-admin.js +6 -4
- package/dist/mcp/mcp-approval.js +8 -1
- package/dist/mcp/mcp-authenticator.js +2 -2
- package/dist/mcp/mcp-server.d.ts +5 -2
- package/dist/mcp/mcp-server.js +6 -4
- package/dist/mcp/mcp-tools.d.ts +1 -1
- package/dist/mcp/mcp-tools.js +1 -1
- package/dist/mcp/mcp-writes.d.ts +6 -2
- package/dist/mcp/mcp-writes.js +15 -6
- package/dist/package.js +1 -1
- package/package.json +4 -4
package/dist/admin-options.d.ts
CHANGED
|
@@ -59,6 +59,9 @@ export type AdminOptions = {
|
|
|
59
59
|
* `writes: true` offers `create_record` and `update_record`, through the REST API's access rules, `validate` and write
|
|
60
60
|
* hooks; the person approves each write in their client, and a client that cannot ask is refused.
|
|
61
61
|
* `writes: { resources: ['tasks'] }` offers them on the listed resources only, and refuses a write to any other.
|
|
62
|
+
* `writes: { rolesWithoutApproval: ['service'] }` lets a user holding one of those roles, such as a service account
|
|
63
|
+
* with no person present, write without the approval; any other user is still asked. Each must be a role the app's
|
|
64
|
+
* users can hold.
|
|
62
65
|
* `false`: the host serves its own MCP endpoint at `auth.mcp.path`, in front of this app. This app then answers 404
|
|
63
66
|
* there and needs no `forAudience`, while the OAuth provider's discovery stays.
|
|
64
67
|
*/
|
package/dist/create-admin.js
CHANGED
|
@@ -23,7 +23,7 @@ import { realtimePath, restPath, storagePath, supabaseFunctionsPath, supabasePat
|
|
|
23
23
|
import { goTruePath } from "./gotrue/keys.js";
|
|
24
24
|
import { authBasePath } from "./better-auth/auth-base-path.js";
|
|
25
25
|
import { mcpDiscovery } from "./better-auth/mcp/discovery-routes.js";
|
|
26
|
-
import { checkMcpWrites } from "./mcp/mcp-writes.js";
|
|
26
|
+
import { checkMcpWrites, rolesWithoutApproval } from "./mcp/mcp-writes.js";
|
|
27
27
|
import { mcpAuthenticator } from "./mcp/mcp-authenticator.js";
|
|
28
28
|
import { mcpToolContext } from "./mcp/mcp-context.js";
|
|
29
29
|
import { mcpHandler } from "./mcp/mcp-handler.js";
|
|
@@ -128,7 +128,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
128
128
|
checkRoleLimits(registry, options.roles?.limits ?? []);
|
|
129
129
|
checkOwnerFields(registry, authenticate.userIdType, auth);
|
|
130
130
|
const mcpAuthenticate = mcpAuthenticator(authenticate, auth?.mcp, options.mcp);
|
|
131
|
-
if (options.mcp) checkMcpWrites(registry, options.mcp.writes);
|
|
131
|
+
if (options.mcp) checkMcpWrites(registry, declaredRoles(auth, options.roles), options.mcp.writes);
|
|
132
132
|
checkPositionAfter(registry);
|
|
133
133
|
checkParents(registry);
|
|
134
134
|
checkPages(pageModels, {
|
|
@@ -299,10 +299,11 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
299
299
|
const report = options.onUnhandledError ?? ((error) => console.error(error));
|
|
300
300
|
if (auth?.mcp && options.mcp === false) closedMcpRoutes(app, auth.mcp.path);
|
|
301
301
|
else if (auth?.mcp && mcpAuthenticate) {
|
|
302
|
+
const writes = options.mcp !== false && options.mcp?.writes || false;
|
|
302
303
|
const tools = mcpTools({
|
|
303
304
|
db,
|
|
304
305
|
sql: options.mcp !== false && options.mcp?.sql === true,
|
|
305
|
-
writes
|
|
306
|
+
writes
|
|
306
307
|
});
|
|
307
308
|
const server = (session) => mcpServer({
|
|
308
309
|
info: {
|
|
@@ -311,7 +312,8 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
311
312
|
},
|
|
312
313
|
tools: tools(session),
|
|
313
314
|
context: mcpToolContext(deps, meta, session),
|
|
314
|
-
report
|
|
315
|
+
report,
|
|
316
|
+
rolesWithoutApproval: rolesWithoutApproval(writes)
|
|
315
317
|
});
|
|
316
318
|
mcpRoutes(app, {
|
|
317
319
|
path: auth.mcp.path,
|
package/dist/mcp/mcp-approval.js
CHANGED
|
@@ -18,6 +18,13 @@ const mcpApproval = (inputResponses) => (request) => {
|
|
|
18
18
|
if (answer.kind === "elicit" && answer.action === "accept" && answer.content?.approve === true) return Promise.resolve("approved");
|
|
19
19
|
return Promise.resolve("rejected");
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* Whether `roles`, the roles the signed-in user holds, include one of `exempt`, whose MCP writes need no approval: an
|
|
23
|
+
* exact match, and none for anything but a list.
|
|
24
|
+
*/
|
|
25
|
+
const writesUnasked = (exempt, roles) => Array.isArray(roles) && roles.some((role) => exempt.includes(role));
|
|
26
|
+
/** `ToolContext.approve` over MCP for a user whose writes need no approval: approved, without asking the client. */
|
|
27
|
+
const approvedUnasked = () => Promise.resolve("approved");
|
|
21
28
|
const asksForms = (capabilities) => {
|
|
22
29
|
const elicitation = capabilities?.elicitation;
|
|
23
30
|
return elicitation !== void 0 && (elicitation.form !== void 0 || elicitation.url === void 0);
|
|
@@ -54,4 +61,4 @@ const approvalAsked = (request, capabilities) => {
|
|
|
54
61
|
}) } });
|
|
55
62
|
};
|
|
56
63
|
//#endregion
|
|
57
|
-
export { ApprovalNeeded, approvalAsked, mcpApproval };
|
|
64
|
+
export { ApprovalNeeded, approvalAsked, approvedUnasked, mcpApproval, writesUnasked };
|
|
@@ -5,11 +5,11 @@ const isMcpOptions = (options) => options === void 0 || options === false || typ
|
|
|
5
5
|
/**
|
|
6
6
|
* The authenticator of the MCP endpoint's tokens, when MCP is on (`createAuth({ mcp })`); none when the host serves its
|
|
7
7
|
* own (`options.mcp: false`). Stops the server at startup when `options.mcp` is neither `false` nor
|
|
8
|
-
* `{ sql?: boolean; writes?: boolean | { resources
|
|
8
|
+
* `{ sql?: boolean; writes?: boolean | { resources?: string[]; rolesWithoutApproval?: string[] } }`, when it is set without MCP, or when `authenticate` cannot verify tokens for
|
|
9
9
|
* another audience.
|
|
10
10
|
*/
|
|
11
11
|
const mcpAuthenticator = (authenticate, mcp, options) => {
|
|
12
|
-
if (!isMcpOptions(options)) throw new Error("options.mcp takes false or { sql?: boolean, writes?: boolean | { resources
|
|
12
|
+
if (!isMcpOptions(options)) throw new Error("options.mcp takes false or { sql?: boolean, writes?: boolean | { resources?: [<resource>, ...], rolesWithoutApproval?: [<role>, ...] } }");
|
|
13
13
|
if (options === false) return void 0;
|
|
14
14
|
if (options && !mcp) throw new Error("options.mcp needs createAuth({ mcp }) passed as auth");
|
|
15
15
|
if (!mcp) return void 0;
|
package/dist/mcp/mcp-server.d.ts
CHANGED
|
@@ -13,10 +13,13 @@ export type McpServerInput = {
|
|
|
13
13
|
context: Omit<ToolContext, 'approve'>;
|
|
14
14
|
/** Gets the errors a tool throws, which the client only sees as a failure. */
|
|
15
15
|
report: (error: unknown) => void;
|
|
16
|
+
/** The roles whose calls are approved without asking the client: a user holding one of them is never asked. None by default. */
|
|
17
|
+
rolesWithoutApproval?: readonly string[];
|
|
16
18
|
};
|
|
17
19
|
/**
|
|
18
20
|
* An MCP server offering `tools` as MCP tools, each with its JSON Schema, its hints, and the text it returns. A tool
|
|
19
|
-
* that asks for approval makes the call ask the client's user, and runs again from the start with their answer
|
|
21
|
+
* that asks for approval makes the call ask the client's user, and runs again from the start with their answer; for a
|
|
22
|
+
* user holding one of `rolesWithoutApproval` it is approved at once, and the client is never asked.
|
|
20
23
|
*/
|
|
21
|
-
export declare const mcpServer: ({ info, tools, context, report }: McpServerInput) => McpServer;
|
|
24
|
+
export declare const mcpServer: ({ info, tools, context, report, rolesWithoutApproval }: McpServerInput) => McpServer;
|
|
22
25
|
//#endregion
|
package/dist/mcp/mcp-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { checkedTools } from "../assistant/checked-tools.js";
|
|
2
|
-
import { ApprovalNeeded, approvalAsked, mcpApproval } from "./mcp-approval.js";
|
|
2
|
+
import { ApprovalNeeded, approvalAsked, approvedUnasked, mcpApproval, writesUnasked } from "./mcp-approval.js";
|
|
3
3
|
import { CLIENT_CAPABILITIES_META_KEY, McpServer, fromJsonSchema } from "@modelcontextprotocol/server";
|
|
4
4
|
//#region src/mcp/mcp-server.ts
|
|
5
5
|
const capabilitiesOf = (envelope) => envelope?.[CLIENT_CAPABILITIES_META_KEY];
|
|
@@ -12,10 +12,12 @@ const failed = {
|
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
14
14
|
* An MCP server offering `tools` as MCP tools, each with its JSON Schema, its hints, and the text it returns. A tool
|
|
15
|
-
* that asks for approval makes the call ask the client's user, and runs again from the start with their answer
|
|
15
|
+
* that asks for approval makes the call ask the client's user, and runs again from the start with their answer; for a
|
|
16
|
+
* user holding one of `rolesWithoutApproval` it is approved at once, and the client is never asked.
|
|
16
17
|
*/
|
|
17
|
-
const mcpServer = ({ info, tools, context, report }) => {
|
|
18
|
+
const mcpServer = ({ info, tools, context, report, rolesWithoutApproval = [] }) => {
|
|
18
19
|
const server = new McpServer(info);
|
|
20
|
+
const unasked = writesUnasked(rolesWithoutApproval, context.session.user.roles);
|
|
19
21
|
for (const tool of checkedTools(tools)) {
|
|
20
22
|
const config = {
|
|
21
23
|
description: tool.description,
|
|
@@ -28,7 +30,7 @@ const mcpServer = ({ info, tools, context, report }) => {
|
|
|
28
30
|
type: "text",
|
|
29
31
|
text: await tool.run(args, {
|
|
30
32
|
...context,
|
|
31
|
-
approve: mcpApproval(ctx.mcpReq.inputResponses)
|
|
33
|
+
approve: unasked ? approvedUnasked : mcpApproval(ctx.mcpReq.inputResponses)
|
|
32
34
|
})
|
|
33
35
|
}] };
|
|
34
36
|
} catch (error) {
|
package/dist/mcp/mcp-tools.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export type McpToolsInput = {
|
|
|
9
9
|
sql: boolean;
|
|
10
10
|
/**
|
|
11
11
|
* Offers `create_record` and `update_record`, on the listed resources only when given `{ resources }`; each write still
|
|
12
|
-
* needs the caller's role and their approval in the client
|
|
12
|
+
* needs the caller's role, and their approval in the client unless they hold one of `rolesWithoutApproval`.
|
|
13
13
|
*/
|
|
14
14
|
writes: McpWrites;
|
|
15
15
|
};
|
package/dist/mcp/mcp-tools.js
CHANGED
|
@@ -9,7 +9,7 @@ import { writeRecordTools } from "../assistant/write-record-tools.js";
|
|
|
9
9
|
*/
|
|
10
10
|
const mcpTools = ({ db, sql, writes }) => (session) => [
|
|
11
11
|
...readRecordTools(),
|
|
12
|
-
...writes === false ? [] : writeRecordTools(writes === true ? {} : { resources: writes.resources }),
|
|
12
|
+
...writes === false ? [] : writeRecordTools(writes === true || !writes.resources ? {} : { resources: writes.resources }),
|
|
13
13
|
...sql && seesAssistant(session.user.roles) ? [readOnlyQueryTool(db)] : []
|
|
14
14
|
];
|
|
15
15
|
//#endregion
|
package/dist/mcp/mcp-writes.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import "../registry.js";
|
|
2
2
|
//#region src/mcp/mcp-writes.d.ts
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Which resources the MCP write tools may change: none, every one the caller may write, or the ones named; and the
|
|
5
|
+
* roles whose writes run without the person's approval in the client.
|
|
6
|
+
*/
|
|
4
7
|
export type McpWrites = boolean | {
|
|
5
|
-
resources
|
|
8
|
+
resources?: string[];
|
|
9
|
+
rolesWithoutApproval?: string[];
|
|
6
10
|
};
|
|
7
11
|
//#endregion
|
package/dist/mcp/mcp-writes.js
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
|
+
import { anonymousRole } from "@protobase/schema";
|
|
1
2
|
//#region src/mcp/mcp-writes.ts
|
|
2
|
-
const
|
|
3
|
+
const isNameList = (value) => Array.isArray(value) && value.every((name) => typeof name === "string" && name !== "");
|
|
4
|
+
const isWritesEntry = ([name, value]) => name === "resources" && isNameList(value) && value.length > 0 || name === "rolesWithoutApproval" && isNameList(value);
|
|
3
5
|
const isMcpWrites = (value) => {
|
|
4
6
|
if (typeof value === "boolean") return true;
|
|
5
7
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
6
8
|
const entries = Object.entries(value);
|
|
7
|
-
return entries.length
|
|
9
|
+
return entries.length > 0 && entries.every(isWritesEntry);
|
|
8
10
|
};
|
|
9
|
-
/**
|
|
10
|
-
const
|
|
11
|
+
/** The roles `writes` lets write without approval; none unless named. */
|
|
12
|
+
const rolesWithoutApproval = (writes) => typeof writes === "object" ? writes.rolesWithoutApproval ?? [] : [];
|
|
13
|
+
/**
|
|
14
|
+
* Stops startup when `options.mcp.writes` names a resource the app does not have, or lets a role write without
|
|
15
|
+
* approval that is not one of `roles`, the roles its users can hold, or is the anonymous role.
|
|
16
|
+
*/
|
|
17
|
+
const checkMcpWrites = (registry, roles, writes) => {
|
|
11
18
|
if (typeof writes !== "object") return;
|
|
12
|
-
const unknown = writes.resources.filter((name) => !registry.find(name));
|
|
19
|
+
const unknown = (writes.resources ?? []).filter((name) => !registry.find(name));
|
|
13
20
|
if (unknown.length > 0) throw new Error(`options.mcp.writes names resources the app does not have: ${unknown.join(", ")}`);
|
|
21
|
+
const unheld = rolesWithoutApproval(writes).filter((role) => role === anonymousRole || !(roles ?? []).includes(role));
|
|
22
|
+
if (unheld.length > 0) throw new Error(`options.mcp.writes.rolesWithoutApproval names roles the app's users cannot hold: ${unheld.join(", ")}`);
|
|
14
23
|
};
|
|
15
24
|
//#endregion
|
|
16
|
-
export { checkMcpWrites, isMcpWrites };
|
|
25
|
+
export { checkMcpWrites, isMcpWrites, rolesWithoutApproval };
|
package/dist/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.1",
|
|
4
4
|
"description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"@better-auth/passkey": "^1.7.12",
|
|
37
37
|
"@better-auth/sso": "^1.7.12",
|
|
38
38
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
39
|
-
"@protobase/layout": "0.41.
|
|
40
|
-
"@protobase/query": "0.41.
|
|
41
|
-
"@protobase/schema": "0.41.
|
|
39
|
+
"@protobase/layout": "0.41.1",
|
|
40
|
+
"@protobase/query": "0.41.1",
|
|
41
|
+
"@protobase/schema": "0.41.1",
|
|
42
42
|
"@react-email/render": "^2.1.0",
|
|
43
43
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
44
44
|
"@sentry/core": "^11.7.0",
|