@protobase/server 0.50.1 → 0.51.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/assistant/delete-record-tool.js +11 -11
- package/dist/assistant/record-before.js +20 -0
- package/dist/assistant/tools.d.ts +3 -0
- package/dist/assistant/write-messages.js +41 -0
- package/dist/assistant/write-prompts.js +79 -0
- package/dist/assistant/write-record-tools.js +9 -12
- package/dist/create-admin.js +15 -11
- package/dist/locale/messages.js +8 -0
- package/dist/locale/request-locale.js +14 -0
- package/dist/mcp/mcp-approval.js +19 -8
- package/dist/mcp/mcp-context.d.ts +5 -3
- package/dist/mcp/mcp-context.js +5 -3
- package/dist/mcp/mcp-handler.d.ts +5 -2
- package/dist/mcp/mcp-handler.js +1 -1
- package/dist/mcp/mcp-server.js +1 -1
- package/dist/package.js +1 -1
- package/package.json +4 -4
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { defineTool } from "./tools.js";
|
|
2
2
|
import { keyOf, keyParameter, refused, resourceOf } from "./record-tool-args.js";
|
|
3
|
+
import { deleteApproval } from "./write-prompts.js";
|
|
4
|
+
import { approvalTexts, recordBefore } from "./record-before.js";
|
|
3
5
|
import { onlyNote, outside, resourceParameterFor } from "./write-tool-limits.js";
|
|
4
6
|
//#region src/assistant/delete-record-tool.ts
|
|
7
|
+
/**
|
|
8
|
+
* `delete_record`: deleting one record as the caller, through the REST API's delete path (its access rules, ETag check,
|
|
9
|
+
* write hooks and soft delete), after the user approves it. Before asking it only reads: the resource's config and the
|
|
10
|
+
* record the approval names, since MCP runs it again from the start with the user's answer.
|
|
11
|
+
*/
|
|
5
12
|
const parameterNames = [
|
|
6
13
|
"resource",
|
|
7
14
|
"key",
|
|
@@ -16,15 +23,6 @@ const deleteKeyOf = (value) => {
|
|
|
16
23
|
};
|
|
17
24
|
const etagOf = (value) => typeof value === "string" && /^"[\x21\x23-\x2b\x2d-\x7e]+"$/.test(value) ? value : void 0;
|
|
18
25
|
const usage = "Pass only the resource, key and etag as { \"resource\": \"<name>\", \"key\": \"<key>\", \"etag\": \"<the etag get_record gave>\" }; nothing was deleted.";
|
|
19
|
-
const approvalOf = (resource, shownKey, softDelete) => ({
|
|
20
|
-
title: softDelete ? `Delete this ${resource} record? It can be restored: the record is kept, marked deleted.` : `Delete this ${resource} record for good? This cannot be undone: the record is removed.`,
|
|
21
|
-
fields: [{
|
|
22
|
-
label: "Key",
|
|
23
|
-
value: shownKey
|
|
24
|
-
}],
|
|
25
|
-
tone: softDelete ? "warning" : "danger",
|
|
26
|
-
approveLabel: softDelete ? "Delete" : "Delete for good"
|
|
27
|
-
});
|
|
28
26
|
/** Deletes one record by its key and ETag once the user approves it, under the caller's access rules. */
|
|
29
27
|
const deleteRecordTool = (input = {}) => defineTool({
|
|
30
28
|
name: "delete_record",
|
|
@@ -60,8 +58,10 @@ const deleteRecordTool = (input = {}) => defineTool({
|
|
|
60
58
|
if (refusal) return refusal;
|
|
61
59
|
const softDelete = await context.records.softDeleteField(resource).then((field) => ({ field }), refused);
|
|
62
60
|
if (typeof softDelete === "string") return softDelete;
|
|
63
|
-
const
|
|
64
|
-
|
|
61
|
+
const before = await recordBefore(context, resource, key, etag);
|
|
62
|
+
if (typeof before === "string") return before;
|
|
63
|
+
const { text, described } = await approvalTexts(context, resource);
|
|
64
|
+
const answer = await context.approve(deleteApproval(text, described, key, before?.record ?? {}, softDelete.field !== void 0));
|
|
65
65
|
if (answer !== "approved") return `The user did not approve this delete (${answer}); nothing was deleted.`;
|
|
66
66
|
const deleted = () => JSON.stringify({
|
|
67
67
|
deleted: true,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { HttpProblem, preconditionFailed } from "../problem.js";
|
|
2
|
+
import { ifMatchSatisfied } from "../etag.js";
|
|
3
|
+
import { refused } from "./record-tool-args.js";
|
|
4
|
+
import { writeText } from "./write-messages.js";
|
|
5
|
+
import { describedResource } from "./write-prompts.js";
|
|
6
|
+
//#region src/assistant/record-before.ts
|
|
7
|
+
const hidden = (error) => error instanceof HttpProblem && (error.status === 403 || error.status === 404);
|
|
8
|
+
/**
|
|
9
|
+
* The record as the caller reads it now, for what an approval names and shows before a write: undefined when they may
|
|
10
|
+
* not read it. A record changed since `etag` is refused before asking, as the write would refuse it after the user
|
|
11
|
+
* approved; so is any other refusal of the read, as the model's to read.
|
|
12
|
+
*/
|
|
13
|
+
const recordBefore = (context, resource, key, etag) => context.records.get(resource, key).then((current) => etag && !ifMatchSatisfied(etag, current.etag) ? refused(preconditionFailed("The record changed since it was read; fetch it again")) : current, (error) => hidden(error) ? void 0 : refused(error));
|
|
14
|
+
/** What an approval is written with: the texts of the user's language, and the resource as the caller may read it. */
|
|
15
|
+
const approvalTexts = async (context, resource) => ({
|
|
16
|
+
text: writeText(context.locale ?? "en"),
|
|
17
|
+
described: describedResource(await context.resources(), resource)
|
|
18
|
+
});
|
|
19
|
+
//#endregion
|
|
20
|
+
export { approvalTexts, recordBefore };
|
|
@@ -2,6 +2,7 @@ import { Session } from "../types.js";
|
|
|
2
2
|
import { ToolRecords } from "./records.js";
|
|
3
3
|
import { Conversation } from "./conversations.js";
|
|
4
4
|
import { ApprovalAnswer, ApprovalRequest } from "./approval.js";
|
|
5
|
+
import { Locale } from "@protobase/schema/internal";
|
|
5
6
|
import { AssistantPart, ResourceModel, ViewModel } from "@protobase/schema";
|
|
6
7
|
//#region src/assistant/tools.d.ts
|
|
7
8
|
/** The resources a caller may read, each restricted to their fields, with their views. */
|
|
@@ -11,6 +12,8 @@ export type ReadableResources = {
|
|
|
11
12
|
};
|
|
12
13
|
export type ToolContext = {
|
|
13
14
|
session: Session;
|
|
15
|
+
/** The language the user reads, for the texts a tool shows them such as its approvals; English when absent. */
|
|
16
|
+
locale?: Locale;
|
|
14
17
|
/** The dock's conversation; absent over MCP. */
|
|
15
18
|
conversation?: Conversation;
|
|
16
19
|
/** Adds a part, such as a table of results, to the assistant message the turn is writing. */
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineMessages } from "../locale/messages.js";
|
|
2
|
+
//#region src/assistant/write-messages.ts
|
|
3
|
+
/** The texts of the approvals the write tools ask for, which the user reads in their client. */
|
|
4
|
+
const writeText = defineMessages({
|
|
5
|
+
en: {
|
|
6
|
+
createTitle: (resource, title) => `Create ${resource}${title === void 0 ? "" : ` "${title}"`}?`,
|
|
7
|
+
updateTitle: (record) => `Update ${record}?`,
|
|
8
|
+
deleteTitle: (record) => `Delete ${record}?`,
|
|
9
|
+
create: "Create",
|
|
10
|
+
update: "Update",
|
|
11
|
+
delete: "Delete",
|
|
12
|
+
deleteForGood: "Delete for good",
|
|
13
|
+
reject: "Cancel",
|
|
14
|
+
noChanges: "No field changes.",
|
|
15
|
+
restorable: "It can be restored: the record is kept, marked deleted.",
|
|
16
|
+
permanent: "This cannot be undone: the record is removed for good.",
|
|
17
|
+
empty: "empty",
|
|
18
|
+
hidden: "hidden",
|
|
19
|
+
yes: "Yes",
|
|
20
|
+
no: "No"
|
|
21
|
+
},
|
|
22
|
+
nl: {
|
|
23
|
+
createTitle: (resource, title) => `${resource}${title === void 0 ? "" : ` "${title}"`} aanmaken?`,
|
|
24
|
+
updateTitle: (record) => `${record} bijwerken?`,
|
|
25
|
+
deleteTitle: (record) => `${record} verwijderen?`,
|
|
26
|
+
create: "Aanmaken",
|
|
27
|
+
update: "Bijwerken",
|
|
28
|
+
delete: "Verwijderen",
|
|
29
|
+
deleteForGood: "Definitief verwijderen",
|
|
30
|
+
reject: "Annuleren",
|
|
31
|
+
noChanges: "Er verandert geen veld.",
|
|
32
|
+
restorable: "Het kan worden teruggezet: het record blijft bewaard, gemarkeerd als verwijderd.",
|
|
33
|
+
permanent: "Dit kan niet ongedaan worden gemaakt: het record wordt definitief verwijderd.",
|
|
34
|
+
empty: "leeg",
|
|
35
|
+
hidden: "verborgen",
|
|
36
|
+
yes: "Ja",
|
|
37
|
+
no: "Nee"
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
//#endregion
|
|
41
|
+
export { writeText };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { humanize, recordTitle } from "@protobase/schema/internal";
|
|
2
|
+
import { enumLabel } from "@protobase/schema";
|
|
3
|
+
//#region src/assistant/write-prompts.ts
|
|
4
|
+
const describedResource = (readable, name) => ({
|
|
5
|
+
name,
|
|
6
|
+
model: readable.models.find((model) => model.name === name),
|
|
7
|
+
view: readable.views.find((view) => view.resource === name)
|
|
8
|
+
});
|
|
9
|
+
const hiddenCharacters = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
10
|
+
const longest = 200;
|
|
11
|
+
/** `text` on one line with nothing hidden in it, cut at 200 characters. */
|
|
12
|
+
const shown = (text) => {
|
|
13
|
+
const line = text.replace(hiddenCharacters, " ");
|
|
14
|
+
return line.length > longest ? `${line.slice(0, 199)}…` : line;
|
|
15
|
+
};
|
|
16
|
+
const resourceLabel = ({ name, view }) => shown(view?.names?.singular ?? humanize(name));
|
|
17
|
+
const fieldLabel = ({ view }, name) => shown(view?.fields[name]?.label ?? humanize(name));
|
|
18
|
+
const shownValue = (text, described, name, value) => {
|
|
19
|
+
const field = described.model?.fields[name];
|
|
20
|
+
if (field?.sensitive) return text.hidden;
|
|
21
|
+
if (value === null || value === void 0) return text.empty;
|
|
22
|
+
if (typeof value === "boolean") return value ? text.yes : text.no;
|
|
23
|
+
if (field?.type === "enum" && typeof value === "string") return shown(enumLabel(value, described.view?.fields[name]?.valueLabels));
|
|
24
|
+
if (typeof value === "string") return shown(value);
|
|
25
|
+
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
26
|
+
return shown(JSON.stringify(value));
|
|
27
|
+
};
|
|
28
|
+
/** `Task #42 "Fix the sign-in lockout"`: the resource's label, the record's key, and its title when it has one apart from the key. */
|
|
29
|
+
const recordName = (described, key, record) => {
|
|
30
|
+
const shownKey = shown(Array.isArray(key) ? key.join(",") : String(key));
|
|
31
|
+
const title = described.model ? shown(recordTitle(described.model, described.view, record, shownKey)) : shownKey;
|
|
32
|
+
return `${resourceLabel(described)} #${shownKey}${title === shownKey ? "" : ` "${title}"`}`;
|
|
33
|
+
};
|
|
34
|
+
const titleOfNew = (described, data) => {
|
|
35
|
+
if (!described.model) return void 0;
|
|
36
|
+
const title = shown(recordTitle(described.model, described.view, data, ""));
|
|
37
|
+
return title === "" ? void 0 : title;
|
|
38
|
+
};
|
|
39
|
+
const same = (before, after) => JSON.stringify(before ?? null) === JSON.stringify(after ?? null);
|
|
40
|
+
/** `Create Task "Fix the sign-in lockout"?`, with each field the new record is given by its label. */
|
|
41
|
+
const createApproval = (text, described, data) => ({
|
|
42
|
+
title: text.createTitle(resourceLabel(described), titleOfNew(described, data)),
|
|
43
|
+
fields: Object.entries(data).map(([name, value]) => ({
|
|
44
|
+
label: fieldLabel(described, name),
|
|
45
|
+
value: shownValue(text, described, name, value)
|
|
46
|
+
})),
|
|
47
|
+
approveLabel: text.create,
|
|
48
|
+
rejectLabel: text.reject
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* `Update Task #42 "Fix the sign-in lockout"?`, with each field that changes as `In flight -> Done`. A field the
|
|
52
|
+
* caller cannot read before, such as a sensitive one, shows its new value alone.
|
|
53
|
+
*/
|
|
54
|
+
const updateApproval = (text, described, key, current, data) => {
|
|
55
|
+
const fields = Object.entries(data).filter(([name, value]) => !Object.hasOwn(current, name) || !same(current[name], value)).map(([name, value]) => {
|
|
56
|
+
const after = shownValue(text, described, name, value);
|
|
57
|
+
const before = Object.hasOwn(current, name) ? `${shownValue(text, described, name, current[name])} -> ` : "";
|
|
58
|
+
return {
|
|
59
|
+
label: fieldLabel(described, name),
|
|
60
|
+
value: `${before}${after}`
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
title: text.updateTitle(recordName(described, key, current)),
|
|
65
|
+
...fields.length > 0 ? { fields } : { body: text.noChanges },
|
|
66
|
+
approveLabel: text.update,
|
|
67
|
+
rejectLabel: text.reject
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
/** `Delete Task #42 "Fix the sign-in lockout"?`, saying whether it can be restored (a soft delete) or is gone for good. */
|
|
71
|
+
const deleteApproval = (text, described, key, current, restorable) => ({
|
|
72
|
+
title: text.deleteTitle(recordName(described, key, current)),
|
|
73
|
+
body: restorable ? text.restorable : text.permanent,
|
|
74
|
+
tone: restorable ? "warning" : "danger",
|
|
75
|
+
approveLabel: restorable ? text.delete : text.deleteForGood,
|
|
76
|
+
rejectLabel: text.reject
|
|
77
|
+
});
|
|
78
|
+
//#endregion
|
|
79
|
+
export { createApproval, deleteApproval, describedResource, updateApproval };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { defineTool } from "./tools.js";
|
|
2
2
|
import { keyOf, keyParameter, refused, resourceOf } from "./record-tool-args.js";
|
|
3
|
+
import { createApproval, updateApproval } from "./write-prompts.js";
|
|
4
|
+
import { approvalTexts, recordBefore } from "./record-before.js";
|
|
3
5
|
import { forbiddenText, notApproved, onlyNote, outside, resourceParameterFor } from "./write-tool-limits.js";
|
|
4
6
|
import { deleteRecordTool } from "./delete-record-tool.js";
|
|
5
7
|
//#region src/assistant/write-record-tools.ts
|
|
@@ -32,11 +34,8 @@ const createRecordTool = (input = {}) => defineTool({
|
|
|
32
34
|
if (!resource || !data) return "Pass the resource and data as { \"resource\": \"<name>\", \"data\": { \"<field>\": <value> } }.";
|
|
33
35
|
const refusal = outside(input, resource);
|
|
34
36
|
if (refusal) return refusal;
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
code: JSON.stringify(data, null, 2),
|
|
38
|
-
approveLabel: "Create"
|
|
39
|
-
});
|
|
37
|
+
const { text, described } = await approvalTexts(context, resource);
|
|
38
|
+
const answer = await context.approve(createApproval(text, described, data));
|
|
40
39
|
if (answer !== "approved") return notApproved(answer);
|
|
41
40
|
return context.records.create(resource, data).then((record) => JSON.stringify(record), refused);
|
|
42
41
|
}
|
|
@@ -76,14 +75,12 @@ const updateRecordTool = (input = {}) => defineTool({
|
|
|
76
75
|
const softDelete = await context.records.softDeleteField(resource).then((field) => ({ field }), refused);
|
|
77
76
|
if (typeof softDelete === "string") return softDelete;
|
|
78
77
|
if (softDelete.field && Object.hasOwn(data, softDelete.field)) return forbiddenText(`update_record does not set ${softDelete.field}, which deleting sets; delete with delete_record. Nothing was written.`);
|
|
79
|
-
const shownKey = Array.isArray(key) ? key.join(",") : String(key);
|
|
80
|
-
const answer = await context.approve({
|
|
81
|
-
title: `Update ${resource} ${shownKey}?`,
|
|
82
|
-
code: JSON.stringify(data, null, 2),
|
|
83
|
-
approveLabel: "Update"
|
|
84
|
-
});
|
|
85
|
-
if (answer !== "approved") return notApproved(answer);
|
|
86
78
|
const etag = typeof args.etag === "string" && args.etag ? args.etag : void 0;
|
|
79
|
+
const before = await recordBefore(context, resource, key, etag);
|
|
80
|
+
if (typeof before === "string") return before;
|
|
81
|
+
const { text, described } = await approvalTexts(context, resource);
|
|
82
|
+
const answer = await context.approve(updateApproval(text, described, key, before?.record ?? {}, data));
|
|
83
|
+
if (answer !== "approved") return notApproved(answer);
|
|
87
84
|
return context.records.update(resource, key, data, { etag }).then((record) => JSON.stringify(record), refused);
|
|
88
85
|
}
|
|
89
86
|
});
|
package/dist/create-admin.js
CHANGED
|
@@ -80,6 +80,7 @@ import { functionRoutes } from "./functions/routes.js";
|
|
|
80
80
|
import { supabaseFunctionRoutes } from "./functions/supabase-routes.js";
|
|
81
81
|
import { upgradeAuthenticator } from "./functions/upgrade-caller.js";
|
|
82
82
|
import { localeSetting } from "./locale/locale-option.js";
|
|
83
|
+
import { requestLocale } from "./locale/request-locale.js";
|
|
83
84
|
import { mapSettings } from "./maps/map-settings.js";
|
|
84
85
|
import { pluginOptions, pluginsBeforeSignIn } from "./plugins/plugin-settings.js";
|
|
85
86
|
import { runtimeUrl } from "./runtime/runtime-settings.js";
|
|
@@ -329,17 +330,20 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
329
330
|
writes
|
|
330
331
|
});
|
|
331
332
|
const approvals = mcpApprovals();
|
|
332
|
-
const server = (session) =>
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
333
|
+
const server = (session, request) => {
|
|
334
|
+
const context = mcpToolContext(deps, meta, session, request ? requestLocale(locale, request) : locale);
|
|
335
|
+
return mcpServer({
|
|
336
|
+
info: {
|
|
337
|
+
name: appName,
|
|
338
|
+
version: "1.0.0"
|
|
339
|
+
},
|
|
340
|
+
tools: tools(session),
|
|
341
|
+
context,
|
|
342
|
+
report,
|
|
343
|
+
rolesWithoutApproval: rolesWithoutApproval(writes),
|
|
344
|
+
approvals
|
|
345
|
+
});
|
|
346
|
+
};
|
|
343
347
|
mcpRoutes(app, {
|
|
344
348
|
path: auth.mcp.path,
|
|
345
349
|
handler: mcpHandler({
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/locale/messages.ts
|
|
2
|
+
/**
|
|
3
|
+
* An area's built-in texts in every language the screens come in: the lookup gives those of one locale, as the server
|
|
4
|
+
* has no single active one. Dutch must translate every English text, which the types check.
|
|
5
|
+
*/
|
|
6
|
+
const defineMessages = (catalogue) => (locale) => catalogue[locale];
|
|
7
|
+
//#endregion
|
|
8
|
+
export { defineMessages };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { pickLocale } from "@protobase/schema/internal";
|
|
2
|
+
//#region src/locale/request-locale.ts
|
|
3
|
+
const acceptedTags = (header) => header.split(",").map((entry) => {
|
|
4
|
+
const [tag = "", ...params] = entry.trim().split(";");
|
|
5
|
+
const weight = params.map((param) => param.trim()).find((param) => param.startsWith("q="));
|
|
6
|
+
return {
|
|
7
|
+
tag: tag.trim(),
|
|
8
|
+
q: weight ? Number(weight.slice(2)) : 1
|
|
9
|
+
};
|
|
10
|
+
}).filter(({ tag, q }) => tag !== "" && tag !== "*" && q > 0).toSorted((a, b) => b.q - a.q).map(({ tag }) => tag);
|
|
11
|
+
/** The language a request's user reads: the app's (`options.locale`) when it sets one, else their `Accept-Language`'s. */
|
|
12
|
+
const requestLocale = (appLocale, request) => appLocale ?? pickLocale(acceptedTags(request.headers.get("accept-language") ?? ""));
|
|
13
|
+
//#endregion
|
|
14
|
+
export { requestLocale };
|
package/dist/mcp/mcp-approval.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { defineMessages } from "../locale/messages.js";
|
|
1
2
|
import { inputRequired, inputResponse } from "@modelcontextprotocol/server";
|
|
2
3
|
//#region src/mcp/mcp-approval.ts
|
|
3
4
|
/** Thrown by `approve` over MCP when the request carries no answer yet: the server then asks the client for one. */
|
|
@@ -41,20 +42,30 @@ const messageOf = (request) => [
|
|
|
41
42
|
request.fields?.map(({ label, value }) => `${label}: ${value}`).join("\n"),
|
|
42
43
|
request.code
|
|
43
44
|
].filter(Boolean).join("\n\n");
|
|
44
|
-
const
|
|
45
|
+
const text = defineMessages({
|
|
46
|
+
en: {
|
|
47
|
+
cannotAsk: "This client cannot ask you to approve a change, so nothing was written; use a client that supports MCP elicitation.",
|
|
48
|
+
approve: "Approve"
|
|
49
|
+
},
|
|
50
|
+
nl: {
|
|
51
|
+
cannotAsk: "Deze client kan je niet vragen een wijziging goed te keuren, dus er is niets opgeslagen; gebruik een client die MCP-elicitation ondersteunt.",
|
|
52
|
+
approve: "Goedkeuren"
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
const cannotAsk = (locale) => ({
|
|
45
56
|
content: [{
|
|
46
57
|
type: "text",
|
|
47
|
-
text:
|
|
58
|
+
text: text(locale).cannotAsk
|
|
48
59
|
}],
|
|
49
60
|
isError: true
|
|
50
|
-
};
|
|
61
|
+
});
|
|
51
62
|
/**
|
|
52
63
|
* The answer to a call whose tool asked for approval: a form asking the user to approve `request` when the client can
|
|
53
|
-
* show one, filed under the nonce of the state `ask` mints for the call, else a refusal
|
|
54
|
-
* declares no capabilities per request, so it is always refused.
|
|
64
|
+
* show one, filed under the nonce of the state `ask` mints for the call, else a refusal in `locale`. A 2025-protocol
|
|
65
|
+
* request declares no capabilities per request, so it is always refused.
|
|
55
66
|
*/
|
|
56
|
-
const approvalAsked = async (request, capabilities, ask) => {
|
|
57
|
-
if (!asksForms(capabilities)) return cannotAsk;
|
|
67
|
+
const approvalAsked = async (request, capabilities, ask, locale = "en") => {
|
|
68
|
+
if (!asksForms(capabilities)) return cannotAsk(locale);
|
|
58
69
|
const { state, nonce } = await ask();
|
|
59
70
|
return inputRequired({
|
|
60
71
|
inputRequests: { [approvalKey(nonce)]: inputRequired.elicit({
|
|
@@ -63,7 +74,7 @@ const approvalAsked = async (request, capabilities, ask) => {
|
|
|
63
74
|
type: "object",
|
|
64
75
|
properties: { approve: {
|
|
65
76
|
type: "boolean",
|
|
66
|
-
title: request.approveLabel ??
|
|
77
|
+
title: request.approveLabel ?? text(locale).approve
|
|
67
78
|
} },
|
|
68
79
|
required: ["approve"]
|
|
69
80
|
}
|
|
@@ -2,10 +2,12 @@ import { Session } from "../types.js";
|
|
|
2
2
|
import { Deps } from "../deps.js";
|
|
3
3
|
import { ToolContext } from "../assistant/tools.js";
|
|
4
4
|
import { Meta } from "../meta.js";
|
|
5
|
+
import { Locale } from "@protobase/schema/internal";
|
|
5
6
|
//#region src/mcp/mcp-context.d.ts
|
|
6
7
|
/**
|
|
7
|
-
* What tools get over MCP for `session`: its records and the resources it may read, under its access rules
|
|
8
|
-
* shows beside a tool's result, and `mcpServer` gives each
|
|
8
|
+
* What tools get over MCP for `session`: its records and the resources it may read, under its access rules, and
|
|
9
|
+
* `locale`, the language its approvals are asked in. Nothing shows beside a tool's result, and `mcpServer` gives each
|
|
10
|
+
* call its own `approve`.
|
|
9
11
|
*/
|
|
10
|
-
export declare const mcpToolContext: (deps: Deps, meta: Pick<Meta, "forCaller">, session: Session) => Omit<ToolContext, "approve">;
|
|
12
|
+
export declare const mcpToolContext: (deps: Deps, meta: Pick<Meta, "forCaller">, session: Session, locale?: Locale) => Omit<ToolContext, "approve">;
|
|
11
13
|
//#endregion
|
package/dist/mcp/mcp-context.js
CHANGED
|
@@ -2,11 +2,13 @@ import { assistantRecords } from "../assistant/records.js";
|
|
|
2
2
|
import { readableResources } from "../assistant/readable-resources.js";
|
|
3
3
|
//#region src/mcp/mcp-context.ts
|
|
4
4
|
/**
|
|
5
|
-
* What tools get over MCP for `session`: its records and the resources it may read, under its access rules
|
|
6
|
-
* shows beside a tool's result, and `mcpServer` gives each
|
|
5
|
+
* What tools get over MCP for `session`: its records and the resources it may read, under its access rules, and
|
|
6
|
+
* `locale`, the language its approvals are asked in. Nothing shows beside a tool's result, and `mcpServer` gives each
|
|
7
|
+
* call its own `approve`.
|
|
7
8
|
*/
|
|
8
|
-
const mcpToolContext = (deps, meta, session) => ({
|
|
9
|
+
const mcpToolContext = (deps, meta, session, locale) => ({
|
|
9
10
|
session,
|
|
11
|
+
...locale && { locale },
|
|
10
12
|
records: assistantRecords(deps, session),
|
|
11
13
|
resources: () => readableResources(meta, session),
|
|
12
14
|
show: () => {}
|
|
@@ -3,8 +3,11 @@ import { McpGateInput } from "./mcp-gate.js";
|
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
4
4
|
//#region src/mcp/mcp-handler.d.ts
|
|
5
5
|
export type McpHandlerInput = McpGateInput & {
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Builds the server for the caller of one request, such as `mcpServer(...)` with tools of your own registered on it;
|
|
8
|
+
* `request` is the HTTP request it serves, such as for the language its user reads.
|
|
9
|
+
*/
|
|
10
|
+
server: (session: Session, request: Request | undefined) => McpServer | Promise<McpServer>;
|
|
8
11
|
};
|
|
9
12
|
/**
|
|
10
13
|
* The MCP endpoint as a fetch handler: refuses a request addressed to another host than the resource's, then one
|
package/dist/mcp/mcp-handler.js
CHANGED
|
@@ -9,7 +9,7 @@ import { createMcpHandler, hostHeaderValidationResponse } from "@modelcontextpro
|
|
|
9
9
|
const mcpHandler = ({ server, ...gate }) => {
|
|
10
10
|
const hostname = new URL(gate.resource).hostname;
|
|
11
11
|
const admit = mcpGate(gate);
|
|
12
|
-
const handler = createMcpHandler((context) => server(sessionOf(context.authInfo)), { legacy: "stateless" });
|
|
12
|
+
const handler = createMcpHandler((context) => server(sessionOf(context.authInfo), context.requestInfo), { legacy: "stateless" });
|
|
13
13
|
return async (request) => {
|
|
14
14
|
const wrongHost = hostHeaderValidationResponse(request, [hostname]);
|
|
15
15
|
if (wrongHost) return wrongHost;
|
package/dist/mcp/mcp-server.js
CHANGED
|
@@ -70,7 +70,7 @@ const mcpServer = ({ info, tools, context, report, rolesWithoutApproval = [], ap
|
|
|
70
70
|
} catch (error) {
|
|
71
71
|
if (!(error instanceof ApprovalNeeded)) throw error;
|
|
72
72
|
const { scope, call } = bound();
|
|
73
|
-
return approvalAsked(error.request, capabilitiesOf(ctx.mcpReq.envelope), () => approvals.ask(scope, call));
|
|
73
|
+
return approvalAsked(error.request, capabilitiesOf(ctx.mcpReq.envelope), () => approvals.ask(scope, call), context.locale);
|
|
74
74
|
}
|
|
75
75
|
};
|
|
76
76
|
server.registerTool(tool.name, config, async (args, ctx) => {
|
package/dist/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.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": {
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"@better-auth/passkey": "^1.7.12",
|
|
41
41
|
"@better-auth/sso": "^1.7.12",
|
|
42
42
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
43
|
-
"@protobase/layout": "0.
|
|
44
|
-
"@protobase/query": "0.
|
|
45
|
-
"@protobase/schema": "0.
|
|
43
|
+
"@protobase/layout": "0.51.1",
|
|
44
|
+
"@protobase/query": "0.51.1",
|
|
45
|
+
"@protobase/schema": "0.51.1",
|
|
46
46
|
"@react-email/render": "^2.1.0",
|
|
47
47
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
48
48
|
"@sentry/core": "^11.7.0",
|