@protobase/server 0.36.0 → 0.37.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/admin-options.d.ts +7 -0
- package/dist/aggregate-groups.js +1 -1
- package/dist/aggregate-service.js +3 -3
- package/dist/aggregate-splits.js +1 -1
- package/dist/assistant/built-in-assistant.js +3 -11
- package/dist/assistant/checked-tools.js +14 -0
- package/dist/assistant/query-tools.js +9 -0
- package/dist/assistant/readable-resources.d.ts +7 -0
- package/dist/assistant/readable-resources.js +13 -0
- package/dist/assistant/record-tools.d.ts +11 -0
- package/dist/assistant/record-tools.js +140 -0
- package/dist/assistant/records.d.ts +27 -0
- package/dist/assistant/records.js +4 -0
- package/dist/assistant/tools.d.ts +19 -2
- package/dist/assistant/turn.d.ts +3 -1
- package/dist/assistant/turn.js +6 -1
- package/dist/comments/mention-job.d.ts +15 -2
- package/dist/comments/mentionable.js +1 -1
- package/dist/comments/read-comments.js +1 -1
- package/dist/create-admin.js +36 -1
- package/dist/deps.d.ts +79 -10
- package/dist/encryption/apply.js +1 -1
- package/dist/encryption/binding.d.ts +1 -1
- package/dist/encryption/keyring.d.ts +12 -0
- package/dist/encryption/runtime.d.ts +8 -1
- package/dist/encryption/scan.d.ts +1 -1
- package/dist/encryption/status.d.ts +1 -1
- package/dist/encryption/status.js +1 -1
- package/dist/files/cleanup-runner.d.ts +5 -0
- package/dist/files/runtime.d.ts +28 -3
- package/dist/files/signer.d.ts +7 -0
- package/dist/functions/records.d.ts +2 -29
- package/dist/functions/records.js +11 -20
- package/dist/history/read-history.js +1 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.js +9 -1
- package/dist/list-service.js +3 -3
- package/dist/map-service.js +1 -1
- package/dist/mcp/mcp-authenticator.js +14 -0
- package/dist/mcp/mcp-context.d.ts +11 -0
- package/dist/mcp/mcp-context.js +16 -0
- package/dist/mcp/mcp-gate.d.ts +19 -0
- package/dist/mcp/mcp-gate.js +52 -0
- package/dist/mcp/mcp-handler.d.ts +15 -0
- package/dist/mcp/mcp-handler.js +22 -0
- package/dist/mcp/mcp-routes.d.ts +12 -0
- package/dist/mcp/mcp-routes.js +15 -0
- package/dist/mcp/mcp-server.d.ts +19 -0
- package/dist/mcp/mcp-server.js +35 -0
- package/dist/mcp/mcp-tools.d.ts +12 -0
- package/dist/mcp/mcp-tools.js +8 -0
- package/dist/meta.d.ts +76 -0
- package/dist/records.js +1 -1
- package/dist/registry.d.ts +22 -2
- package/dist/request-access.d.ts +2 -2
- package/dist/routes/collection.js +1 -1
- package/dist/routes/record.js +1 -1
- package/dist/row-permissions.js +1 -1
- package/dist/scan-guard.d.ts +21 -3
- package/dist/sql-parts.js +1 -1
- package/dist/write-shared.js +1 -1
- package/package.json +5 -4
package/dist/admin-options.d.ts
CHANGED
|
@@ -52,6 +52,13 @@ export type AdminOptions = {
|
|
|
52
52
|
errorReporting?: ErrorReportingOptions | false;
|
|
53
53
|
/** The assistant in the admin app's dock: a backend elsewhere or the built-in one (see `AssistantOptions`); `false` turns it off. */
|
|
54
54
|
assistant?: AssistantOptions | false;
|
|
55
|
+
/**
|
|
56
|
+
* The MCP endpoint, on with `createAuth({ mcp })`. `sql: true` also offers admins and the `ai` role
|
|
57
|
+
* `run_read_only_query`, which reads the whole database as the app's database role, outside the access rules.
|
|
58
|
+
*/
|
|
59
|
+
mcp?: {
|
|
60
|
+
sql?: boolean;
|
|
61
|
+
};
|
|
55
62
|
/**
|
|
56
63
|
* The websites whose browser code may call the API (`/api/*` but `/api/auth/*`) besides the app's own origins (see
|
|
57
64
|
* `createAuth`'s `origins`): a list of origins, or `*` for any. Defaults to the one the platform passes in
|
package/dist/aggregate-groups.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { badRequest } from "./problem.js";
|
|
2
|
-
import { readTransaction } from "./transactions.js";
|
|
3
2
|
import { parseParams } from "./params.js";
|
|
4
3
|
import { labelRequests, relatedLabels } from "./related-labels.js";
|
|
4
|
+
import { readTransaction } from "./transactions.js";
|
|
5
5
|
import { splitGroups } from "./aggregate-splits.js";
|
|
6
6
|
import { scoped, seriesRange } from "./aggregate-service.js";
|
|
7
7
|
import { z } from "zod";
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { badRequest } from "./problem.js";
|
|
2
2
|
import { checkedFilter } from "./filter-input.js";
|
|
3
3
|
import { readScope, requestAccess, requireOperation } from "./request-access.js";
|
|
4
|
-
import { columnId, filterCondition, scopeConditions, tableId, whereClause } from "./sql-parts.js";
|
|
5
|
-
import { readTransaction } from "./transactions.js";
|
|
6
4
|
import { parseParams } from "./params.js";
|
|
5
|
+
import { columnId, filterCondition, scopeConditions, tableId, whereClause } from "./sql-parts.js";
|
|
7
6
|
import { labelRequests, relatedLabels } from "./related-labels.js";
|
|
7
|
+
import { readTransaction } from "./transactions.js";
|
|
8
8
|
import { rangeEnd, rangeStart } from "./range-ends.js";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
-
import { sql } from "kysely";
|
|
11
10
|
import { facetCounts, histogram, series, withoutField } from "@protobase/query";
|
|
11
|
+
import { sql } from "kysely";
|
|
12
12
|
//#region src/aggregate-service.ts
|
|
13
13
|
const field = z.string().min(1);
|
|
14
14
|
const filter = z.string().optional();
|
package/dist/aggregate-splits.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readTransaction } from "./transactions.js";
|
|
2
1
|
import { labelRequests, relatedLabels } from "./related-labels.js";
|
|
2
|
+
import { readTransaction } from "./transactions.js";
|
|
3
3
|
import { series, splitSeries } from "@protobase/query";
|
|
4
4
|
//#region src/aggregate-splits.ts
|
|
5
5
|
/**
|
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
import { openRouterUrl } from "./model-settings.js";
|
|
2
2
|
import { seesAssistant } from "./assistant-settings.js";
|
|
3
3
|
import { appContext } from "./app-context.js";
|
|
4
|
+
import { checkedTools } from "./checked-tools.js";
|
|
4
5
|
import { conversationKey, createConversations } from "./conversations.js";
|
|
5
6
|
import { fileConversationStore } from "./file-conversation-store.js";
|
|
6
7
|
import { assistantProtocolRoutes } from "./protocol-routes.js";
|
|
7
8
|
import { readOnlyQueryTool, readWriteQueryTool } from "./query-tools.js";
|
|
9
|
+
import { readableResources } from "./readable-resources.js";
|
|
8
10
|
import { runTurn } from "./turn.js";
|
|
9
11
|
//#region src/assistant/built-in-assistant.ts
|
|
10
|
-
const toolName = /^[A-Za-z0-9_-]{1,64}$/;
|
|
11
|
-
/** The tools in order; a name a model endpoint refuses, or one taken twice, stops the server at startup. */
|
|
12
|
-
const checkedTools = (tools) => {
|
|
13
|
-
const seen = /* @__PURE__ */ new Set();
|
|
14
|
-
for (const tool of tools) {
|
|
15
|
-
if (!toolName.test(tool.name)) throw new Error(`Assistant tool name "${tool.name}" must be 1 to 64 letters, digits, _ or -`);
|
|
16
|
-
if (seen.has(tool.name)) throw new Error(`Two assistant tools are named "${tool.name}"; give one of them another name`);
|
|
17
|
-
seen.add(tool.name);
|
|
18
|
-
}
|
|
19
|
-
return tools;
|
|
20
|
-
};
|
|
21
12
|
const pageContext = (page) => page && `The user is on the page ${page} of the app (/<resource> lists records, /<resource>/<key> shows one).`;
|
|
22
13
|
/**
|
|
23
14
|
* Protobase's own assistant: answers questions about the app, reads its data with read-only queries and proposes
|
|
@@ -47,6 +38,7 @@ const builtInAssistant = ({ model, chats, db, meta, authenticate, tools: appTool
|
|
|
47
38
|
system: appContext(body.resources, body.views),
|
|
48
39
|
tools,
|
|
49
40
|
...records && { records: records(session) },
|
|
41
|
+
resources: () => readableResources(meta, session),
|
|
50
42
|
report,
|
|
51
43
|
...page && { context: pageContext(page) },
|
|
52
44
|
...onOpenRouter && {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/assistant/checked-tools.ts
|
|
2
|
+
const toolName = /^[A-Za-z0-9_-]{1,64}$/;
|
|
3
|
+
/** The tools in order; a name a model endpoint refuses, or one taken twice, stops the server at startup. */
|
|
4
|
+
const checkedTools = (tools) => {
|
|
5
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6
|
+
for (const tool of tools) {
|
|
7
|
+
if (!toolName.test(tool.name)) throw new Error(`Assistant tool name "${tool.name}" must be 1 to 64 letters, digits, _ or -`);
|
|
8
|
+
if (seen.has(tool.name)) throw new Error(`Two assistant tools are named "${tool.name}"; give one of them another name`);
|
|
9
|
+
seen.add(tool.name);
|
|
10
|
+
}
|
|
11
|
+
return tools;
|
|
12
|
+
};
|
|
13
|
+
//#endregion
|
|
14
|
+
export { checkedTools };
|
|
@@ -36,6 +36,10 @@ const readOnlyQueryTool = (db, options = {}) => defineTool({
|
|
|
36
36
|
properties: { sql: sqlParameter("One SELECT statement, without a trailing semicolon.") },
|
|
37
37
|
required: ["sql"]
|
|
38
38
|
},
|
|
39
|
+
annotations: {
|
|
40
|
+
readOnlyHint: true,
|
|
41
|
+
openWorldHint: false
|
|
42
|
+
},
|
|
39
43
|
run: async (args, context) => {
|
|
40
44
|
const statement = statementOf(args);
|
|
41
45
|
if (!statement) return "Pass the statement as { \"sql\": \"select ...\" }.";
|
|
@@ -60,6 +64,11 @@ const readWriteQueryTool = (db, options = {}) => defineTool({
|
|
|
60
64
|
},
|
|
61
65
|
required: ["sql", "summary"]
|
|
62
66
|
},
|
|
67
|
+
annotations: {
|
|
68
|
+
readOnlyHint: false,
|
|
69
|
+
destructiveHint: true,
|
|
70
|
+
openWorldHint: false
|
|
71
|
+
},
|
|
63
72
|
run: async (args, context) => {
|
|
64
73
|
const statement = statementOf(args);
|
|
65
74
|
if (!statement) return "Pass the statement as { \"sql\": \"update ... returning ...\", \"summary\": \"...\" }.";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Session } from "../types.js";
|
|
2
|
+
import { ReadableResources } from "./tools.js";
|
|
3
|
+
import { Meta } from "../meta.js";
|
|
4
|
+
//#region src/assistant/readable-resources.d.ts
|
|
5
|
+
/** The resources `session` may read or list, leaving out those it may only create in, with their views. */
|
|
6
|
+
export declare const readableResources: (meta: Pick<Meta, "forCaller">, session: Session) => Promise<ReadableResources>;
|
|
7
|
+
//#endregion
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/assistant/readable-resources.ts
|
|
2
|
+
/** The resources `session` may read or list, leaving out those it may only create in, with their views. */
|
|
3
|
+
const readableResources = async (meta, session) => {
|
|
4
|
+
const { body } = await meta.forCaller(session);
|
|
5
|
+
const models = body.resources.filter((model) => body.permissions[model.name]?.read);
|
|
6
|
+
const names = new Set(models.map((model) => model.name));
|
|
7
|
+
return {
|
|
8
|
+
models,
|
|
9
|
+
views: body.views.filter((view) => names.has(view.resource))
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { readableResources };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AssistantTool } from "./tools.js";
|
|
2
|
+
//#region src/assistant/record-tools.d.ts
|
|
3
|
+
/** Lists the resources the caller may read, with their fields, labels and which fields filters and sorts take. */
|
|
4
|
+
export declare const listResourcesTool: () => AssistantTool;
|
|
5
|
+
/** Finds records of a resource with the REST API's filters, sorting and pages, under the caller's access rules. */
|
|
6
|
+
export declare const listRecordsTool: () => AssistantTool;
|
|
7
|
+
/** Reads one record by its key, under the caller's access rules. */
|
|
8
|
+
export declare const getRecordTool: () => AssistantTool;
|
|
9
|
+
/** `list_resources`, `list_records` and `get_record`: reading the app's records as the caller, through the REST API's rules. */
|
|
10
|
+
export declare const readRecordTools: () => AssistantTool[];
|
|
11
|
+
//#endregion
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { HttpProblem } from "../problem.js";
|
|
2
|
+
import { defineTool } from "./tools.js";
|
|
3
|
+
//#region src/assistant/record-tools.ts
|
|
4
|
+
const readOnly = {
|
|
5
|
+
readOnlyHint: true,
|
|
6
|
+
openWorldHint: false
|
|
7
|
+
};
|
|
8
|
+
const defaultPageSize = 20;
|
|
9
|
+
const maxPageSize = 100;
|
|
10
|
+
const refused = (error) => {
|
|
11
|
+
if (error instanceof HttpProblem && error.status < 500) return JSON.stringify({
|
|
12
|
+
error: error.slug,
|
|
13
|
+
message: error.detail
|
|
14
|
+
});
|
|
15
|
+
throw error;
|
|
16
|
+
};
|
|
17
|
+
const fieldEntry = (field, view) => {
|
|
18
|
+
const hint = view?.fields[field.name];
|
|
19
|
+
return {
|
|
20
|
+
name: field.name,
|
|
21
|
+
type: field.type,
|
|
22
|
+
...hint?.label && { label: hint.label },
|
|
23
|
+
...hint?.help && { help: hint.help },
|
|
24
|
+
...field.enumValues && { values: field.enumValues },
|
|
25
|
+
...field.relation && { links_to: field.relation.resource },
|
|
26
|
+
optional: field.nullable,
|
|
27
|
+
read_only: field.readOnly,
|
|
28
|
+
filterable: field.filterable,
|
|
29
|
+
sortable: field.sortable
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const resourceEntry = (model, view) => ({
|
|
33
|
+
name: model.name,
|
|
34
|
+
...view?.names && { label: view.names.plural },
|
|
35
|
+
...view?.help && { help: view.help },
|
|
36
|
+
key: model.primaryKey,
|
|
37
|
+
fields: Object.values(model.fields).map((field) => fieldEntry(field, view))
|
|
38
|
+
});
|
|
39
|
+
const described = ({ models, views }) => models.map((model) => resourceEntry(model, views.find((view) => view.resource === model.name)));
|
|
40
|
+
const resourceOf = (args) => typeof args.resource === "string" && args.resource ? args.resource : void 0;
|
|
41
|
+
const keyOf = (value) => {
|
|
42
|
+
if (typeof value === "string" || typeof value === "number") return value;
|
|
43
|
+
if (Array.isArray(value) && value.length > 0 && value.every((part) => typeof part === "string" || typeof part === "number")) return value;
|
|
44
|
+
};
|
|
45
|
+
const resourceParameter = {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "The resource's name, as list_resources gives it."
|
|
48
|
+
};
|
|
49
|
+
/** Lists the resources the caller may read, with their fields, labels and which fields filters and sorts take. */
|
|
50
|
+
const listResourcesTool = () => defineTool({
|
|
51
|
+
name: "list_resources",
|
|
52
|
+
description: "Lists the resources you may read, as JSON: each with its name, label, key fields and fields (name, type, label, enum values, the resource a relation links to, and whether it is optional, read-only, filterable and sortable). Filters and order_by in list_records take only fields marked filterable or sortable.",
|
|
53
|
+
parameters: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {}
|
|
56
|
+
},
|
|
57
|
+
annotations: readOnly,
|
|
58
|
+
run: async (_args, context) => JSON.stringify(described(await context.resources()))
|
|
59
|
+
});
|
|
60
|
+
/** Finds records of a resource with the REST API's filters, sorting and pages, under the caller's access rules. */
|
|
61
|
+
const listRecordsTool = () => defineTool({
|
|
62
|
+
name: "list_records",
|
|
63
|
+
description: `Finds records of a resource and returns a page of them as JSON ({ items, next_page_token, total_size_estimate }). filter uses AIP-160 syntax, such as status = "open" AND amount > 10, and order_by a comma-separated list of fields, each optionally followed by desc. Pass next_page_token back as page_token for the next page; it is empty on the last.`,
|
|
64
|
+
parameters: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: {
|
|
67
|
+
resource: resourceParameter,
|
|
68
|
+
filter: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "An AIP-160 filter on filterable fields, such as status = \"open\" AND amount > 10."
|
|
71
|
+
},
|
|
72
|
+
order_by: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "Sortable fields, comma-separated, each optionally followed by desc, such as created_at desc."
|
|
75
|
+
},
|
|
76
|
+
page_size: {
|
|
77
|
+
type: "integer",
|
|
78
|
+
minimum: 1,
|
|
79
|
+
maximum: maxPageSize,
|
|
80
|
+
description: `Records per page, ${defaultPageSize} by default.`
|
|
81
|
+
},
|
|
82
|
+
page_token: {
|
|
83
|
+
type: "string",
|
|
84
|
+
description: "The next_page_token of the previous page."
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
required: ["resource"]
|
|
88
|
+
},
|
|
89
|
+
annotations: readOnly,
|
|
90
|
+
run: async (args, context) => {
|
|
91
|
+
const resource = resourceOf(args);
|
|
92
|
+
if (!resource) return "Pass the resource as { \"resource\": \"<name>\" }.";
|
|
93
|
+
const params = {
|
|
94
|
+
...typeof args.filter === "string" && { filter: args.filter },
|
|
95
|
+
...typeof args.order_by === "string" && { order_by: args.order_by },
|
|
96
|
+
...typeof args.page_token === "string" && { page_token: args.page_token },
|
|
97
|
+
page_size: typeof args.page_size === "number" ? Math.min(args.page_size, maxPageSize) : defaultPageSize
|
|
98
|
+
};
|
|
99
|
+
return context.records.list(resource, params).then((page) => JSON.stringify(page), refused);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
/** Reads one record by its key, under the caller's access rules. */
|
|
103
|
+
const getRecordTool = () => defineTool({
|
|
104
|
+
name: "get_record",
|
|
105
|
+
description: "Reads one record of a resource by its key and returns it as JSON ({ record, etag }).",
|
|
106
|
+
parameters: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: {
|
|
109
|
+
resource: resourceParameter,
|
|
110
|
+
key: {
|
|
111
|
+
description: "The record's key: its value, or the values of its key fields in order when the key has several.",
|
|
112
|
+
anyOf: [
|
|
113
|
+
{ type: "string" },
|
|
114
|
+
{ type: "number" },
|
|
115
|
+
{
|
|
116
|
+
type: "array",
|
|
117
|
+
items: { type: ["string", "number"] },
|
|
118
|
+
minItems: 1
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
required: ["resource", "key"]
|
|
124
|
+
},
|
|
125
|
+
annotations: readOnly,
|
|
126
|
+
run: async (args, context) => {
|
|
127
|
+
const resource = resourceOf(args);
|
|
128
|
+
const key = keyOf(args.key);
|
|
129
|
+
if (!resource || key === void 0) return "Pass the resource and key as { \"resource\": \"<name>\", \"key\": \"<key>\" }.";
|
|
130
|
+
return context.records.get(resource, key).then((record) => JSON.stringify(record), refused);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
/** `list_resources`, `list_records` and `get_record`: reading the app's records as the caller, through the REST API's rules. */
|
|
134
|
+
const readRecordTools = () => [
|
|
135
|
+
listResourcesTool(),
|
|
136
|
+
listRecordsTool(),
|
|
137
|
+
getRecordTool()
|
|
138
|
+
];
|
|
139
|
+
//#endregion
|
|
140
|
+
export { getRecordTool, listRecordsTool, listResourcesTool, readRecordTools };
|
|
@@ -8,11 +8,38 @@ export type ToolRecord = {
|
|
|
8
8
|
record: Row;
|
|
9
9
|
etag: string;
|
|
10
10
|
};
|
|
11
|
+
/** The list parameters of the REST API (AIP-132), as values rather than query text. */
|
|
12
|
+
export type ListParams = {
|
|
13
|
+
filter?: string;
|
|
14
|
+
order_by?: string;
|
|
15
|
+
page_size?: number;
|
|
16
|
+
page_token?: string;
|
|
17
|
+
fields?: string[];
|
|
18
|
+
count?: 'exact';
|
|
19
|
+
show_deleted?: boolean;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A page of records as the REST API lists it: each item with its `etag` and, unless the resource opts out, whether the
|
|
23
|
+
* caller may update and delete it; and the token of the next page, empty on the last.
|
|
24
|
+
*/
|
|
25
|
+
export type RecordPage = {
|
|
26
|
+
items: Array<Row & {
|
|
27
|
+
etag?: string;
|
|
28
|
+
permissions?: {
|
|
29
|
+
update: boolean;
|
|
30
|
+
delete: boolean;
|
|
31
|
+
};
|
|
32
|
+
}>;
|
|
33
|
+
next_page_token: string;
|
|
34
|
+
total_size_estimate: number;
|
|
35
|
+
total_size?: number;
|
|
36
|
+
};
|
|
11
37
|
/**
|
|
12
38
|
* The app's resources as the session's user: their access rules, tenant scope, validation and write hooks apply as
|
|
13
39
|
* in the REST API. A refusal or a missing record rejects with an `HttpProblem`, which a tool can turn into text.
|
|
14
40
|
*/
|
|
15
41
|
export type ToolRecords = {
|
|
42
|
+
list: (resource: string, params?: ListParams) => Promise<RecordPage>;
|
|
16
43
|
get: (resource: string, key: RecordKeyInput) => Promise<ToolRecord>;
|
|
17
44
|
create: (resource: string, data: Row) => Promise<ToolRecord>;
|
|
18
45
|
/** With `etag`, fails with 412 when the record changed since it was read; without, it updates any version. */
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { notFound } from "../problem.js";
|
|
2
2
|
import { decodeKeyText } from "../key.js";
|
|
3
|
+
import { listRecords } from "../list-service.js";
|
|
3
4
|
import { readRecord } from "../read-service.js";
|
|
4
5
|
import { runWrite } from "../write-pipeline.js";
|
|
5
6
|
import { encodeKey } from "@protobase/schema/internal";
|
|
6
7
|
//#region src/assistant/records.ts
|
|
8
|
+
const filterLength = 2e5;
|
|
7
9
|
/** `ToolRecords` over the admin's resources for `session`. */
|
|
8
10
|
const assistantRecords = (deps, session) => {
|
|
9
11
|
const entryOf = (resource) => {
|
|
@@ -19,6 +21,7 @@ const assistantRecords = (deps, session) => {
|
|
|
19
21
|
};
|
|
20
22
|
};
|
|
21
23
|
return {
|
|
24
|
+
list: async (resource, params = {}) => (await listRecords(deps, entryOf(resource), session, params, filterLength)).body,
|
|
22
25
|
get: async (resource, input) => {
|
|
23
26
|
const { entry, key } = keyOf(resource, input);
|
|
24
27
|
const { record, etag } = await readRecord(deps, entry, session, key);
|
|
@@ -45,6 +48,7 @@ const assistantRecords = (deps, session) => {
|
|
|
45
48
|
const withoutAdmin = () => Promise.reject(/* @__PURE__ */ new Error("This assistant has no records: ToolContext.records works in the built-in assistant"));
|
|
46
49
|
/** Records for a turn without an admin around it, such as in a backend of its own: every call fails. */
|
|
47
50
|
const noRecords = {
|
|
51
|
+
list: withoutAdmin,
|
|
48
52
|
get: withoutAdmin,
|
|
49
53
|
create: withoutAdmin,
|
|
50
54
|
update: withoutAdmin
|
|
@@ -2,17 +2,33 @@ 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 { AssistantPart } from "@protobase/schema";
|
|
5
|
+
import { AssistantPart, ResourceModel, ViewModel } from "@protobase/schema";
|
|
6
6
|
//#region src/assistant/tools.d.ts
|
|
7
|
+
/** The resources a caller may read, each restricted to their fields, with their views. */
|
|
8
|
+
export type ReadableResources = {
|
|
9
|
+
models: ResourceModel[];
|
|
10
|
+
views: ViewModel[];
|
|
11
|
+
};
|
|
7
12
|
export type ToolContext = {
|
|
8
13
|
session: Session;
|
|
9
|
-
conversation
|
|
14
|
+
/** The dock's conversation; absent over MCP. */
|
|
15
|
+
conversation?: Conversation;
|
|
10
16
|
/** Adds a part, such as a table of results, to the assistant message the turn is writing. */
|
|
11
17
|
show: (part: AssistantPart) => void;
|
|
12
18
|
/** Asks the user to approve with a card in that message (see `requestApproval`). */
|
|
13
19
|
approve: (request: ApprovalRequest) => Promise<ApprovalAnswer>;
|
|
14
20
|
/** The app's resources as the session's user, through their access rules and write hooks (built-in assistant only). */
|
|
15
21
|
records: ToolRecords;
|
|
22
|
+
/** The resources the caller may read, each restricted to their fields, with their views. */
|
|
23
|
+
resources: () => Promise<ReadableResources>;
|
|
24
|
+
};
|
|
25
|
+
/** MCP's tool hints, which an MCP client may show or act on; the dock ignores them. */
|
|
26
|
+
export type ToolAnnotations = {
|
|
27
|
+
title?: string;
|
|
28
|
+
readOnlyHint?: boolean;
|
|
29
|
+
destructiveHint?: boolean;
|
|
30
|
+
idempotentHint?: boolean;
|
|
31
|
+
openWorldHint?: boolean;
|
|
16
32
|
};
|
|
17
33
|
/**
|
|
18
34
|
* A function the model may call during a turn. `parameters` is the JSON Schema of its arguments; `run` gets them
|
|
@@ -23,6 +39,7 @@ export type AssistantTool = {
|
|
|
23
39
|
description: string;
|
|
24
40
|
parameters: Record<string, unknown>;
|
|
25
41
|
run: (args: Record<string, unknown>, context: ToolContext) => Promise<string>;
|
|
42
|
+
annotations?: ToolAnnotations;
|
|
26
43
|
};
|
|
27
44
|
/** Typed identity, for a tool of your own. */
|
|
28
45
|
export declare const defineTool: (tool: AssistantTool) => AssistantTool;
|
package/dist/assistant/turn.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Session } from "../types.js";
|
|
|
2
2
|
import { ModelSettings, ReasoningEffort } from "./chat-completions.js";
|
|
3
3
|
import { ToolRecords } from "./records.js";
|
|
4
4
|
import { Conversation } from "./conversations.js";
|
|
5
|
-
import { AssistantTool } from "./tools.js";
|
|
5
|
+
import { AssistantTool, ReadableResources } from "./tools.js";
|
|
6
6
|
//#region src/assistant/turn.d.ts
|
|
7
7
|
export type TurnOptions = {
|
|
8
8
|
model: ModelSettings;
|
|
@@ -13,6 +13,8 @@ export type TurnOptions = {
|
|
|
13
13
|
tools?: AssistantTool[];
|
|
14
14
|
/** What tools get as `records`; without it, their calls fail. */
|
|
15
15
|
records?: ToolRecords;
|
|
16
|
+
/** What tools get as `resources`; without it, no resources. */
|
|
17
|
+
resources?: () => Promise<ReadableResources>;
|
|
16
18
|
/** Model calls in one turn before it stops. Default 8. */
|
|
17
19
|
maxSteps?: number;
|
|
18
20
|
/** Gets errors other than the model endpoint's refusals, which the user only sees as a failure. */
|
package/dist/assistant/turn.js
CHANGED
|
@@ -3,6 +3,10 @@ import { ModelError, streamChatCompletion } from "./chat-completions.js";
|
|
|
3
3
|
import { requestApproval } from "./approval.js";
|
|
4
4
|
import { withCacheBreakpoints } from "./cache-breakpoints.js";
|
|
5
5
|
//#region src/assistant/turn.ts
|
|
6
|
+
const noResources = async () => ({
|
|
7
|
+
models: [],
|
|
8
|
+
views: []
|
|
9
|
+
});
|
|
6
10
|
const toolResult = async (call, tools, context) => {
|
|
7
11
|
const tool = tools.find((entry) => entry.name === call.function.name);
|
|
8
12
|
if (!tool) return `There is no tool named "${call.function.name}".`;
|
|
@@ -41,7 +45,8 @@ const runTurn = async (options, text) => {
|
|
|
41
45
|
conversation,
|
|
42
46
|
show,
|
|
43
47
|
approve: (request) => requestApproval(conversation, messageId, request),
|
|
44
|
-
records: options.records ?? noRecords
|
|
48
|
+
records: options.records ?? noRecords,
|
|
49
|
+
resources: options.resources ?? noResources
|
|
45
50
|
};
|
|
46
51
|
const specs = tools.map((tool) => ({
|
|
47
52
|
type: "function",
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
-
import "
|
|
1
|
+
import { JobDefinition } from "../jobs/job-types.js";
|
|
2
|
+
import { CommentUsers } from "./comment-users.js";
|
|
2
3
|
import "../index.js";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
//#region src/comments/mention-job.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The job that emails someone a comment mentions, enqueued with the comment: nothing when the app has no mail, when an
|
|
8
|
+
* admin deleted the comment since, or when the user is banned or gone. A refused send is retried.
|
|
9
|
+
*/
|
|
10
|
+
export declare const mentionJob: (users: CommentUsers | undefined, appName: string) => JobDefinition<{
|
|
11
|
+
comment: string;
|
|
12
|
+
user: string;
|
|
13
|
+
url: string;
|
|
14
|
+
record: string;
|
|
15
|
+
}>;
|
|
16
|
+
//#endregion
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { notFound } from "../problem.js";
|
|
2
|
-
import { readTransaction } from "../transactions.js";
|
|
3
2
|
import { parseParams } from "../params.js";
|
|
3
|
+
import { readTransaction } from "../transactions.js";
|
|
4
4
|
import { commentAccess, mayRead, readableRecord, requireSignedIn, userSessions } from "./comment-access.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
//#region src/comments/mentionable.ts
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { notFound } from "../problem.js";
|
|
2
2
|
import { tenantScope } from "../tenant.js";
|
|
3
|
+
import { parseParams } from "../params.js";
|
|
3
4
|
import { readTransaction } from "../transactions.js";
|
|
4
5
|
import { commentsTable } from "./comments-sql.js";
|
|
5
|
-
import { parseParams } from "../params.js";
|
|
6
6
|
import { commentAccess, readableRecord } from "./comment-access.js";
|
|
7
7
|
import { commentColumns, commentItem, namedUsers } from "./comment-item.js";
|
|
8
8
|
import { encodeKey } from "@protobase/schema/internal";
|
package/dist/create-admin.js
CHANGED
|
@@ -23,6 +23,12 @@ 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 { mcpAuthenticator } from "./mcp/mcp-authenticator.js";
|
|
27
|
+
import { mcpToolContext } from "./mcp/mcp-context.js";
|
|
28
|
+
import { mcpHandler } from "./mcp/mcp-handler.js";
|
|
29
|
+
import { mcpRoutes } from "./mcp/mcp-routes.js";
|
|
30
|
+
import { mcpServer } from "./mcp/mcp-server.js";
|
|
31
|
+
import { mcpTools } from "./mcp/mcp-tools.js";
|
|
26
32
|
import { withoutSessionTokens } from "./better-auth/without-session-tokens.js";
|
|
27
33
|
import { organizationRoutes } from "./better-auth/organizations/admin-routes.js";
|
|
28
34
|
import { statusRoute } from "./better-auth/status-route.js";
|
|
@@ -120,6 +126,7 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
120
126
|
checkViews(registry, viewModels, pageModels.map((entry) => entry.name), fieldTypes);
|
|
121
127
|
checkRoleLimits(registry, options.roles?.limits ?? []);
|
|
122
128
|
checkOwnerFields(registry, authenticate.userIdType, auth);
|
|
129
|
+
const mcpAuthenticate = mcpAuthenticator(authenticate, auth?.mcp, options.mcp);
|
|
123
130
|
checkPositionAfter(registry);
|
|
124
131
|
checkParents(registry);
|
|
125
132
|
checkPages(pageModels, {
|
|
@@ -241,7 +248,11 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
241
248
|
const corsOrigins = withMobileOrigins(options.cors ?? readCorsOrigins(globalThis.process?.env ?? {}), Boolean(mobile));
|
|
242
249
|
const ownOrigins = auth?.origins ?? [];
|
|
243
250
|
if (corsOrigins !== void 0 || ownOrigins.length > 1) {
|
|
244
|
-
const answering = [
|
|
251
|
+
const answering = [
|
|
252
|
+
authBasePath,
|
|
253
|
+
...auth?.mcp ? [auth.mcp.path] : [],
|
|
254
|
+
...Object.entries(apiFunctions).filter(([, fn]) => fn.options.cors).flatMap(([name]) => [`${systemPath}/functions/${name}`, `${supabaseFunctionsPath}/${name}`])
|
|
255
|
+
];
|
|
245
256
|
const skip = (path) => answering.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
|
|
246
257
|
const cors = apiCors({
|
|
247
258
|
own: ownOrigins,
|
|
@@ -283,6 +294,30 @@ const createAdmin = ({ resources: appResources, views: appViews = [], pages = []
|
|
|
283
294
|
}));
|
|
284
295
|
}
|
|
285
296
|
const report = options.onUnhandledError ?? ((error) => console.error(error));
|
|
297
|
+
if (auth?.mcp && mcpAuthenticate) {
|
|
298
|
+
const tools = mcpTools({
|
|
299
|
+
db,
|
|
300
|
+
sql: options.mcp?.sql === true
|
|
301
|
+
});
|
|
302
|
+
const server = (session) => mcpServer({
|
|
303
|
+
info: {
|
|
304
|
+
name: appName,
|
|
305
|
+
version: "1.0.0"
|
|
306
|
+
},
|
|
307
|
+
tools: tools(session),
|
|
308
|
+
context: mcpToolContext(deps, meta, session),
|
|
309
|
+
report
|
|
310
|
+
});
|
|
311
|
+
mcpRoutes(app, {
|
|
312
|
+
path: auth.mcp.path,
|
|
313
|
+
handler: mcpHandler({
|
|
314
|
+
authenticate: mcpAuthenticate,
|
|
315
|
+
resource: auth.mcp.resource,
|
|
316
|
+
scope: auth.mcp.scope,
|
|
317
|
+
server
|
|
318
|
+
})
|
|
319
|
+
});
|
|
320
|
+
}
|
|
286
321
|
const upgradeAuthenticate = upgradeAuthenticator({
|
|
287
322
|
authenticate,
|
|
288
323
|
...auth && {
|
package/dist/deps.d.ts
CHANGED
|
@@ -1,12 +1,81 @@
|
|
|
1
|
-
import "./types.js";
|
|
2
|
-
import "./comments/comment-users.js";
|
|
3
|
-
import "./
|
|
4
|
-
import "./
|
|
5
|
-
import "./
|
|
6
|
-
import "./
|
|
7
|
-
import "./
|
|
8
|
-
import "./
|
|
1
|
+
import { AuditQueue, PipelineHook } from "./types.js";
|
|
2
|
+
import { CommentUsers } from "./comments/comment-users.js";
|
|
3
|
+
import { ErrorReportingSettings } from "./error-reporting/error-reporting-settings.js";
|
|
4
|
+
import { mentionJob } from "./comments/mention-job.js";
|
|
5
|
+
import { ModelSettings } from "./assistant/chat-completions.js";
|
|
6
|
+
import { EncryptionRuntime } from "./encryption/runtime.js";
|
|
7
|
+
import { FilesRuntime } from "./files/runtime.js";
|
|
8
|
+
import { MapSettings } from "./maps/map-settings.js";
|
|
9
|
+
import { PluginOption } from "./plugins/plugin-settings.js";
|
|
10
|
+
import { Registry } from "./registry.js";
|
|
11
|
+
import { SourceOption } from "./sources/source-settings.js";
|
|
12
|
+
import { ScanGuard } from "./scan-guard.js";
|
|
9
13
|
import { PageModel } from "@protobase/layout";
|
|
10
14
|
import { Locale } from "@protobase/schema/internal";
|
|
11
|
-
import { ResolveOptions, ViewModel } from "@protobase/schema";
|
|
12
|
-
import { Db } from "@protobase/query";
|
|
15
|
+
import { BottomBarModel, ResolveOptions, UserMenuModel, ViewModel } from "@protobase/schema";
|
|
16
|
+
import { Db } from "@protobase/query";
|
|
17
|
+
//#region src/deps.d.ts
|
|
18
|
+
/** Everything a route needs, built once by `createAdmin` and passed down explicitly. */
|
|
19
|
+
export type Deps = {
|
|
20
|
+
db: Db;
|
|
21
|
+
registry: Registry;
|
|
22
|
+
views: ViewModel[];
|
|
23
|
+
userMenu?: UserMenuModel;
|
|
24
|
+
/** The bottom bars, in config order. */
|
|
25
|
+
bottomBars: BottomBarModel[];
|
|
26
|
+
pages: PageModel[];
|
|
27
|
+
basePath: string;
|
|
28
|
+
/** Where the system endpoints live (`/meta`, `/openapi.json`, `/docs`): the parent of `basePath`, `/api` by default. */
|
|
29
|
+
systemPath: string;
|
|
30
|
+
statementTimeoutMs: number;
|
|
31
|
+
guard: ScanGuard;
|
|
32
|
+
hooks: readonly PipelineHook[];
|
|
33
|
+
accessOptions: ResolveOptions;
|
|
34
|
+
/** Where reveals of sensitive fields are published. */
|
|
35
|
+
audit: AuditQueue;
|
|
36
|
+
/** Whether list items of a resource carry their permissions. */
|
|
37
|
+
rowPermissions: (resource: string) => boolean;
|
|
38
|
+
/** The URL of the assistant backend that `/meta` names to callers who may use it: another origin's, or the built-in one's path. */
|
|
39
|
+
assistant?: string;
|
|
40
|
+
/** The URL of the runtime updates endpoint that `/meta` names to admins. */
|
|
41
|
+
runtime?: string;
|
|
42
|
+
/** Where the admin app sends its errors, which `/meta` names to everyone. */
|
|
43
|
+
errorReporting?: ErrorReportingSettings;
|
|
44
|
+
/** The map style the admin app draws maps with and the layers they may switch to, which `/meta` names to everyone. */
|
|
45
|
+
maps?: MapSettings;
|
|
46
|
+
/** The language the admin app's screens are in for everyone, which `/meta` names to everyone. */
|
|
47
|
+
locale?: Locale;
|
|
48
|
+
/** The plugins `/meta` names to the callers they are for. */
|
|
49
|
+
plugins: PluginOption[];
|
|
50
|
+
/** The other APIs whose resources `/meta` names to the callers they are for. */
|
|
51
|
+
sources: SourceOption[];
|
|
52
|
+
/** Providers, signing and scheduled deletes, when a resource has a file field. */
|
|
53
|
+
files?: FilesRuntime;
|
|
54
|
+
/** The keys encrypted fields are sealed and revealed with, when a resource has one. */
|
|
55
|
+
encryption?: EncryptionRuntime;
|
|
56
|
+
/** The transaction setting that tells the change capture who writes, when a resource has events or history (`changes.actor`). */
|
|
57
|
+
actorSetting?: string;
|
|
58
|
+
/** Where record history is kept, when a resource has `.history()`, and the names of the users who made changes. */
|
|
59
|
+
history?: {
|
|
60
|
+
schema: string;
|
|
61
|
+
actorNames?: (ids: readonly string[]) => Promise<Record<string, string>>;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* When a resource has `.comments()`: with the project's auth, its users and the job that mails a mention; and the
|
|
65
|
+
* origin of the app for a request's URL, which the mailed links go to.
|
|
66
|
+
*/
|
|
67
|
+
comments?: {
|
|
68
|
+
users?: CommentUsers;
|
|
69
|
+
mentionJob?: ReturnType<typeof mentionJob>;
|
|
70
|
+
appOrigin: (requestURL: string) => string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* When a resource has an AI field or a view an AI action: the model they call (none without a key), and where a model
|
|
74
|
+
* that fails an action is reported.
|
|
75
|
+
*/
|
|
76
|
+
ai?: {
|
|
77
|
+
model?: ModelSettings;
|
|
78
|
+
report?: (error: unknown) => void;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
//#endregion
|