@raybase/core 0.3.6 → 0.3.7
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/agent-ai-chat.js +2 -2
- package/dist/agent-ai-chat.js.map +1 -1
- package/dist/audited-write.d.ts +19 -0
- package/dist/audited-write.d.ts.map +1 -0
- package/dist/audited-write.js +42 -0
- package/dist/audited-write.js.map +1 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +21 -8
- package/dist/auth.js.map +1 -1
- package/dist/data-client.d.ts +8 -1
- package/dist/data-client.d.ts.map +1 -1
- package/dist/data-client.js +29 -22
- package/dist/data-client.js.map +1 -1
- package/dist/db.d.ts +13 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +3 -2
- package/dist/db.js.map +1 -1
- package/dist/do.d.ts +5 -0
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +5 -0
- package/dist/do.js.map +1 -1
- package/dist/email.d.ts.map +1 -1
- package/dist/email.js +13 -4
- package/dist/email.js.map +1 -1
- package/dist/html.d.ts.map +1 -1
- package/dist/html.js +11 -13
- package/dist/html.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +23 -5
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +180 -44
- package/dist/mcp.js.map +1 -1
- package/dist/operator-mcp.js +1 -1
- package/dist/operator-mcp.js.map +1 -1
- package/dist/same-origin-path.d.ts +37 -0
- package/dist/same-origin-path.d.ts.map +1 -0
- package/dist/same-origin-path.js +29 -0
- package/dist/same-origin-path.js.map +1 -0
- package/dist/storage-client.d.ts.map +1 -1
- package/dist/storage-client.js +44 -28
- package/dist/storage-client.js.map +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/vite.d.ts +76 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +121 -0
- package/dist/vite.js.map +1 -1
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +71 -41
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
package/dist/mcp.js
CHANGED
|
@@ -2,17 +2,21 @@ import { z } from "zod";
|
|
|
2
2
|
import { RAYBASE_CORE_VERSION } from "./version.js";
|
|
3
3
|
import { withHostUserRoleCeiling } from "./host-user.js";
|
|
4
4
|
import { redactSecretColumnKeys, secretColumnKeys } from "./data-secrets.js";
|
|
5
|
+
import { actorAuditRef, recordAudit } from "./audit.js";
|
|
5
6
|
import { audit, base64url, normalizeRoles } from "./auth.js";
|
|
7
|
+
import { servicePrincipal } from "./principal.js";
|
|
6
8
|
import { hasAdminAccess, normalizeStoredAdminRole } from "./admin-role.js";
|
|
7
9
|
import { inputSchemaForTable } from "./data-client.js";
|
|
8
10
|
import { createRaybaseDb, dataStub } from "./db.js";
|
|
9
11
|
import { sha256Hex } from "./internal.js";
|
|
10
|
-
import {
|
|
12
|
+
import { actionInputIssues } from "./actions.js";
|
|
13
|
+
import { jsonSchemaFor, parseWithSchema } from "./json-schema.js";
|
|
11
14
|
import { searchIndex, updateSearchForDataChange } from "./search.js";
|
|
12
15
|
import { id, longText, raybaseSystemTable, text } from "./schema.js";
|
|
13
16
|
import { randomId } from "./sql.js";
|
|
14
17
|
import { wallNowIso as nowIso, wallNowMs } from "./clock.js";
|
|
15
18
|
import { ensureManifestTables } from "./system-tables.js";
|
|
19
|
+
import { sameOriginPath } from "./same-origin-path.js";
|
|
16
20
|
export const mcpAppsExtensionId = "io.modelcontextprotocol/ui";
|
|
17
21
|
export const mcpAppResourceMimeType = "text/html;profile=mcp-app";
|
|
18
22
|
const jsonHeaders = { "content-type": "application/json; charset=utf-8" };
|
|
@@ -596,7 +600,29 @@ export function wwwAuthenticate(request, config, surface = "app") {
|
|
|
596
600
|
const scope = surface === "operator" ? "operator:read" : "mcp:read mcp:write";
|
|
597
601
|
return `Bearer resource_metadata="${serverOrigin(request)}${metadataPath}", resource="${resource}", scope="${scope}"`;
|
|
598
602
|
}
|
|
599
|
-
|
|
603
|
+
/**
|
|
604
|
+
* The answer to an app connector request that names a host account but has
|
|
605
|
+
* no usable token. The connector's address is the plain one: its protected
|
|
606
|
+
* resource metadata names it without a query (RFC 9728 section 2 discourages
|
|
607
|
+
* one), and section 3.3 forbids a client to use metadata whose resource is not
|
|
608
|
+
* the address it was given. So an address carrying the account cannot get a
|
|
609
|
+
* token; a 401 would leave the client stuck without a word. This says why, and
|
|
610
|
+
* which address to give the agent. The account is asked for when the person
|
|
611
|
+
* connects, and confirmed by the host.
|
|
612
|
+
*/
|
|
613
|
+
function hostAccountAddressResponse(request, config, param) {
|
|
614
|
+
const address = canonicalMcpResource(request, config);
|
|
615
|
+
return jsonResponse({
|
|
616
|
+
error: `This connector's address is ${address}, without "${param}". Give the agent that address; it asks which account to connect when the person signs in.`,
|
|
617
|
+
address
|
|
618
|
+
}, { status: 400, headers: Object.fromEntries(corsHeaders(request, config)) });
|
|
619
|
+
}
|
|
620
|
+
function unauthorizedMcpResponse(request, config, error, surface = "app", options = {}) {
|
|
621
|
+
if (surface === "app" && options.hostApproval) {
|
|
622
|
+
const param = hostAccountParam(options.hostApproval);
|
|
623
|
+
if (new URL(request.url).searchParams.has(param))
|
|
624
|
+
return hostAccountAddressResponse(request, config, param);
|
|
625
|
+
}
|
|
600
626
|
return jsonResponse({ error }, {
|
|
601
627
|
status: 401,
|
|
602
628
|
headers: { "www-authenticate": wwwAuthenticate(request, config, surface), ...Object.fromEntries(corsHeaders(request, config)) }
|
|
@@ -876,6 +902,12 @@ async function registerConfidentialClient(request, config, db, body, user, optio
|
|
|
876
902
|
return jsonResponse({ error: "invalid_client_metadata", error_description: "token_endpoint_auth_method must be client_secret_basic or client_secret_post" }, { status: 400 });
|
|
877
903
|
}
|
|
878
904
|
const resource = asString(body.resource) || canonicalMcpResource(request, config);
|
|
905
|
+
// The credential is for one of this app's own connectors, at the address
|
|
906
|
+
// this request reached: never a resource on another origin.
|
|
907
|
+
if (!mcpSurfaceForResource(request, config, resource, options)) {
|
|
908
|
+
const connectors = mcpSurfaces(request, config, options).map((surface) => surface.resource).join(" or ");
|
|
909
|
+
return jsonResponse({ error: "invalid_target", error_description: `resource must be this app's connector: ${connectors}` }, { status: 400 });
|
|
910
|
+
}
|
|
879
911
|
let created;
|
|
880
912
|
try {
|
|
881
913
|
created = await createMcpMachineCredential(db, config, {
|
|
@@ -945,6 +977,15 @@ export async function createMcpMachineCredential(db, config, input, options = {}
|
|
|
945
977
|
const scopeError = authorizeMcpScopes(input.user, scopes, surface, options);
|
|
946
978
|
if (scopeError)
|
|
947
979
|
throw new McpMachineCredentialError("invalid_scope", scopeError);
|
|
980
|
+
// A credential holds only scopes its connector honours: one no tool or
|
|
981
|
+
// resource answers to would sit on the grant, unexplained, until a later
|
|
982
|
+
// tool happened to take its name. (authorizeMcpScopes already holds the
|
|
983
|
+
// operator connector to its own list.)
|
|
984
|
+
const offered = new Set([...surface.scopesSupported, ...(options.appScopes ?? [])]);
|
|
985
|
+
const unoffered = surface.id === "app" ? scopes.filter((scope) => !offered.has(scope)) : [];
|
|
986
|
+
if (unoffered.length) {
|
|
987
|
+
throw new McpMachineCredentialError("invalid_scope", `${surface.label} does not offer ${unoffered.length === 1 ? "the scope" : "the scopes"} ${unoffered.join(", ")}`);
|
|
988
|
+
}
|
|
948
989
|
const authMethod = input.tokenEndpointAuthMethod ?? "client_secret_basic";
|
|
949
990
|
const user = input.user;
|
|
950
991
|
const clientId = `raybase_cc_${randomId("client")}`;
|
|
@@ -1116,8 +1157,12 @@ function consentStylesheetHref(config) {
|
|
|
1116
1157
|
const href = config.mcp?.consent?.stylesheet?.trim();
|
|
1117
1158
|
if (!href)
|
|
1118
1159
|
return null;
|
|
1119
|
-
if (href.startsWith("/")
|
|
1120
|
-
|
|
1160
|
+
if (href.startsWith("/")) {
|
|
1161
|
+
const path = sameOriginPath(href);
|
|
1162
|
+
if (path)
|
|
1163
|
+
return path;
|
|
1164
|
+
throw new Error("mcp.consent.stylesheet must be a root-relative path or an HTTPS URL");
|
|
1165
|
+
}
|
|
1121
1166
|
try {
|
|
1122
1167
|
if (new URL(href).protocol === "https:")
|
|
1123
1168
|
return href;
|
|
@@ -1258,6 +1303,18 @@ export async function mcpAuthorizationOrganizationHint(request) {
|
|
|
1258
1303
|
const value = asString(input.org).trim();
|
|
1259
1304
|
return value || null;
|
|
1260
1305
|
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Who a grant's writes are audited as. A host-account grant acts as the
|
|
1308
|
+
* host's subject, which is no Raybase user, while `_audit.actorId` names
|
|
1309
|
+
* one; so it is audited as a service principal, the way Core audits every
|
|
1310
|
+
* actor that is not a user, and the account, subject and grant travel in the
|
|
1311
|
+
* entry's metadata. Every other grant acts as its user.
|
|
1312
|
+
*/
|
|
1313
|
+
function mcpGrantAuditActor(grant) {
|
|
1314
|
+
if (grant.hostAccount)
|
|
1315
|
+
return servicePrincipal("raybase-mcp-host-account", { id: grant.subjectId });
|
|
1316
|
+
return actorAuditRef(grant.userId);
|
|
1317
|
+
}
|
|
1261
1318
|
/** The identity a host-account grant acts as: a plain app user, never an operator. */
|
|
1262
1319
|
function hostAccountUser(hostAccount, subject) {
|
|
1263
1320
|
return {
|
|
@@ -1332,13 +1389,18 @@ async function createGrantAndCode(db, config, request, user, values, clientName,
|
|
|
1332
1389
|
addSeconds(300),
|
|
1333
1390
|
now
|
|
1334
1391
|
]);
|
|
1335
|
-
await
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1392
|
+
await recordAudit(db, {
|
|
1393
|
+
actor: mcpGrantAuditActor({ hostAccount, subjectId, userId: user.id }),
|
|
1394
|
+
action: surface.id === "operator" ? "operator_mcp.grant.create" : "mcp.grant.create",
|
|
1395
|
+
target: { type: "_mcp_grants", id: grantId },
|
|
1396
|
+
metadata: {
|
|
1397
|
+
clientId: values.client_id,
|
|
1398
|
+
scopes,
|
|
1399
|
+
resource,
|
|
1400
|
+
surface: surface.id,
|
|
1401
|
+
hostAccount,
|
|
1402
|
+
organizationId
|
|
1403
|
+
}
|
|
1342
1404
|
});
|
|
1343
1405
|
const redirect = new URL(values.redirect_uri);
|
|
1344
1406
|
redirect.searchParams.set("code", code);
|
|
@@ -1383,10 +1445,10 @@ function authorizeMcpScopes(user, scopes, surface, options = {}) {
|
|
|
1383
1445
|
return null;
|
|
1384
1446
|
}
|
|
1385
1447
|
function mcpSignInPath(config) {
|
|
1386
|
-
const
|
|
1387
|
-
if (!
|
|
1388
|
-
throw new Error("mcp.oauth.signInPath must be a root-relative path");
|
|
1389
|
-
return
|
|
1448
|
+
const path = sameOriginPath(config.mcp?.oauth?.signInPath?.trim() || "/login");
|
|
1449
|
+
if (!path)
|
|
1450
|
+
throw new Error("mcp.oauth.signInPath must be a root-relative path on this origin");
|
|
1451
|
+
return path;
|
|
1390
1452
|
}
|
|
1391
1453
|
/** The authorization request as a GET address on this origin, path and query only. */
|
|
1392
1454
|
function authorizationRequestPath(values, choice, input) {
|
|
@@ -1424,10 +1486,11 @@ export async function authorizeMcpOAuthRequest(request, config, db, user, option
|
|
|
1424
1486
|
return redirectWithError(validated.values.redirect_uri, validated.values.state || null, "access_denied");
|
|
1425
1487
|
return createGrantAndCode(db, config, request, user, validated.values, validated.clientName, options, binding);
|
|
1426
1488
|
}
|
|
1427
|
-
const
|
|
1428
|
-
if (
|
|
1429
|
-
|
|
1430
|
-
|
|
1489
|
+
const ownPageSetting = config.mcp?.consent?.path?.trim();
|
|
1490
|
+
if (ownPageSetting) {
|
|
1491
|
+
const ownPage = sameOriginPath(ownPageSetting);
|
|
1492
|
+
if (!ownPage)
|
|
1493
|
+
throw new Error("mcp.consent.path must be a root-relative path on this origin");
|
|
1431
1494
|
const target = new URL(ownPage, request.url);
|
|
1432
1495
|
target.search = new URL(request.url).search;
|
|
1433
1496
|
return new Response(null, { status: 302, headers: { location: target.toString(), "cache-control": "no-store" } });
|
|
@@ -1519,8 +1582,9 @@ export async function handleMcpHostConnect(request, config, options) {
|
|
|
1519
1582
|
/**
|
|
1520
1583
|
* The authorization endpoint when host approval is on: records a pending
|
|
1521
1584
|
* authorization for the requested host account and sends the person to the
|
|
1522
|
-
* host's admin to approve it. With no host account, sends
|
|
1523
|
-
* page first
|
|
1585
|
+
* host's admin to approve it. With no host account, sends the person to the
|
|
1586
|
+
* connect page first; an authorization request for any resource other than
|
|
1587
|
+
* the app connector is refused.
|
|
1524
1588
|
*/
|
|
1525
1589
|
export async function beginMcpHostAuthorization(request, config, db, env, options) {
|
|
1526
1590
|
const limited = checkMcpRateLimit(request, "host-oauth-authorize", 120);
|
|
@@ -1528,7 +1592,10 @@ export async function beginMcpHostAuthorization(request, config, db, env, option
|
|
|
1528
1592
|
return limited;
|
|
1529
1593
|
const param = hostAccountParam(options);
|
|
1530
1594
|
const input = await authorizationInput(request);
|
|
1531
|
-
|
|
1595
|
+
const canonical = canonicalMcpResource(request, config);
|
|
1596
|
+
// Only a request for the connector itself is sent to ask for an account;
|
|
1597
|
+
// one for any other resource is refused below, before anyone types one.
|
|
1598
|
+
if (!input[param] && (!input.resource || input.resource === canonical)) {
|
|
1532
1599
|
const connect = new URL(mcpHostApprovalPath(options, "connectPath"), request.url);
|
|
1533
1600
|
for (const [key, value] of Object.entries(input))
|
|
1534
1601
|
connect.searchParams.set(key, value);
|
|
@@ -1539,8 +1606,8 @@ export async function beginMcpHostAuthorization(request, config, db, env, option
|
|
|
1539
1606
|
return validated;
|
|
1540
1607
|
if (validated.surface.id !== "app")
|
|
1541
1608
|
return jsonResponse({ error: "Host-account MCP grants are only supported for the app MCP resource" }, { status: 400 });
|
|
1542
|
-
const resource = validated.values.resource ||
|
|
1543
|
-
if (resource !==
|
|
1609
|
+
const resource = validated.values.resource || canonical;
|
|
1610
|
+
if (resource !== canonical)
|
|
1544
1611
|
return redirectWithError(validated.values.redirect_uri, validated.values.state || null, "invalid_target");
|
|
1545
1612
|
const hostAccount = options.normalizeHostAccount(input[param]);
|
|
1546
1613
|
const pendingId = randomId("mcp_pending");
|
|
@@ -1828,40 +1895,40 @@ export async function validateMcpBearerToken(request, config, db, expectedSurfac
|
|
|
1828
1895
|
const header = request.headers.get("authorization") ?? "";
|
|
1829
1896
|
const match = /^Bearer\s+(.+)$/i.exec(header);
|
|
1830
1897
|
if (!match) {
|
|
1831
|
-
return unauthorizedMcpResponse(request, config, "Unauthorized", expectedSurface);
|
|
1898
|
+
return unauthorizedMcpResponse(request, config, "Unauthorized", expectedSurface, options);
|
|
1832
1899
|
}
|
|
1833
1900
|
const tokenHash = await sha256Hex(match[1]);
|
|
1834
1901
|
const row = await db.first(`SELECT * FROM "_oauth_access_tokens" WHERE "tokenHash" = ?`, [tokenHash]);
|
|
1835
1902
|
if (!row || row.revokedAt || Date.parse(row.expiresAt) <= wallNowMs()) {
|
|
1836
|
-
return unauthorizedMcpResponse(request, config, "Invalid or expired token", expectedSurface);
|
|
1903
|
+
return unauthorizedMcpResponse(request, config, "Invalid or expired token", expectedSurface, options);
|
|
1837
1904
|
}
|
|
1838
1905
|
const expectedResource = resourceOverride ?? canonicalMcpResourceForSurface(request, config, expectedSurface);
|
|
1839
1906
|
if (row.resource !== expectedResource)
|
|
1840
|
-
return unauthorizedMcpResponse(request, config, "Token audience does not match this MCP resource", expectedSurface);
|
|
1907
|
+
return unauthorizedMcpResponse(request, config, "Token audience does not match this MCP resource", expectedSurface, options);
|
|
1841
1908
|
const grant = await loadGrant(db, row.grantId);
|
|
1842
1909
|
if (!grant || grant.revokedAt || grant.clientId !== row.clientId || grant.resource !== expectedResource) {
|
|
1843
|
-
return unauthorizedMcpResponse(request, config, "MCP grant is no longer active", expectedSurface);
|
|
1910
|
+
return unauthorizedMcpResponse(request, config, "MCP grant is no longer active", expectedSurface, options);
|
|
1844
1911
|
}
|
|
1845
1912
|
if (grant.expiresAt && Date.parse(grant.expiresAt) <= wallNowMs())
|
|
1846
|
-
return unauthorizedMcpResponse(request, config, "MCP grant has expired", expectedSurface);
|
|
1913
|
+
return unauthorizedMcpResponse(request, config, "MCP grant has expired", expectedSurface, options);
|
|
1847
1914
|
const user = await grantUser(db, grant, row.userId, options);
|
|
1848
1915
|
if (!user)
|
|
1849
|
-
return unauthorizedMcpResponse(request, config, "MCP subject is no longer valid", expectedSurface);
|
|
1916
|
+
return unauthorizedMcpResponse(request, config, "MCP subject is no longer valid", expectedSurface, options);
|
|
1850
1917
|
const resourceRequest = resourceOverride ? new Request(expectedResource) : request;
|
|
1851
1918
|
const surface = mcpSurfaceForResource(resourceRequest, config, expectedResource, options);
|
|
1852
1919
|
if (!surface)
|
|
1853
|
-
return unauthorizedMcpResponse(request, config, "Unknown MCP resource", expectedSurface);
|
|
1920
|
+
return unauthorizedMcpResponse(request, config, "Unknown MCP resource", expectedSurface, options);
|
|
1854
1921
|
if (surface.id === "operator") {
|
|
1855
1922
|
if (grant.subjectType !== "raybase-operator" || grant.subjectId !== user.id) {
|
|
1856
|
-
return unauthorizedMcpResponse(request, config, "MCP grant is not valid for Operator MCP", expectedSurface);
|
|
1923
|
+
return unauthorizedMcpResponse(request, config, "MCP grant is not valid for Operator MCP", expectedSurface, options);
|
|
1857
1924
|
}
|
|
1858
1925
|
const grantScopes = splitScopes(parseJson(grant.scopes));
|
|
1859
1926
|
if (authorizeMcpScopes(user, grantScopes, surface, options)) {
|
|
1860
|
-
return unauthorizedMcpResponse(request, config, "Operator MCP grant is no longer authorized", expectedSurface);
|
|
1927
|
+
return unauthorizedMcpResponse(request, config, "Operator MCP grant is no longer authorized", expectedSurface, options);
|
|
1861
1928
|
}
|
|
1862
1929
|
}
|
|
1863
1930
|
else if (grant.subjectType === "raybase-operator" || row.scope.includes("operator:")) {
|
|
1864
|
-
return unauthorizedMcpResponse(request, config, "Operator MCP tokens cannot be used on the app MCP resource", expectedSurface);
|
|
1931
|
+
return unauthorizedMcpResponse(request, config, "Operator MCP tokens cannot be used on the app MCP resource", expectedSurface, options);
|
|
1865
1932
|
}
|
|
1866
1933
|
const tokenHostAccount = grant.hostAccount || undefined;
|
|
1867
1934
|
if (options.hostApproval) {
|
|
@@ -1921,10 +1988,44 @@ function mcpToolDescriptor(tool) {
|
|
|
1921
1988
|
_meta: mcpToolMeta(tool)
|
|
1922
1989
|
};
|
|
1923
1990
|
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Validates a call's arguments against the tool's input schema, Zod or JSON
|
|
1993
|
+
* Schema, the way an action validates its input. A failure comes back as the
|
|
1994
|
+
* issues, each naming the field and what it must be.
|
|
1995
|
+
*/
|
|
1924
1996
|
function parseToolArgs(tool, raw) {
|
|
1925
|
-
|
|
1926
|
-
return tool.inputSchema
|
|
1927
|
-
|
|
1997
|
+
try {
|
|
1998
|
+
return { ok: true, value: parseWithSchema(tool.inputSchema, raw, "arguments") };
|
|
1999
|
+
}
|
|
2000
|
+
catch (error) {
|
|
2001
|
+
return { ok: false, issues: actionInputIssues(error) };
|
|
2002
|
+
}
|
|
2003
|
+
}
|
|
2004
|
+
function invalidToolArgumentsMessage(toolName, issues) {
|
|
2005
|
+
const listed = issues.map((issue) => (issue.path === "(root)" ? issue.message : `${issue.path}: ${issue.message}`));
|
|
2006
|
+
return listed.length
|
|
2007
|
+
? `The arguments for ${toolName} do not match its input schema. ${listed.join("; ")}.`
|
|
2008
|
+
: `The arguments for ${toolName} do not match its input schema.`;
|
|
2009
|
+
}
|
|
2010
|
+
/** The tool result for arguments its schema refuses, shaped like an action's invalid_input error. */
|
|
2011
|
+
function invalidToolArgumentsResult(toolName, issues) {
|
|
2012
|
+
const message = invalidToolArgumentsMessage(toolName, issues);
|
|
2013
|
+
const guidance = issues.length
|
|
2014
|
+
? `Fix the fields named above and call ${toolName} again.`
|
|
2015
|
+
: `Call ${toolName} again with arguments that match its input schema.`;
|
|
2016
|
+
return {
|
|
2017
|
+
content: [{ type: "text", text: `${message} ${guidance}` }],
|
|
2018
|
+
structuredContent: {
|
|
2019
|
+
ok: false,
|
|
2020
|
+
tool: toolName,
|
|
2021
|
+
code: "invalid_input",
|
|
2022
|
+
message,
|
|
2023
|
+
guidance,
|
|
2024
|
+
retryable: true,
|
|
2025
|
+
...(issues.length ? { details: { issues } } : {})
|
|
2026
|
+
},
|
|
2027
|
+
isError: true
|
|
2028
|
+
};
|
|
1928
2029
|
}
|
|
1929
2030
|
function rpcResult(id, result) {
|
|
1930
2031
|
return { jsonrpc: "2.0", id: id ?? null, result };
|
|
@@ -2121,14 +2222,32 @@ async function event(db, grantId, clientId, action, toolName, metadata) {
|
|
|
2121
2222
|
await db.run(`INSERT INTO "_mcp_grant_events" ("id", "grantId", "clientId", "action", "toolName", "metadata", "createdAt")
|
|
2122
2223
|
VALUES (?, ?, ?, ?, ?, ?, ?)`, [randomId("mcp_event"), grantId, clientId, action, toolName ?? null, metadata === undefined ? null : JSON.stringify(metadata), nowIso()]);
|
|
2123
2224
|
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
const secretColumns = secretColumnKeys(options.schema, options.systemTables);
|
|
2127
|
-
const allTools = [
|
|
2225
|
+
function connectorTools(options) {
|
|
2226
|
+
return [
|
|
2128
2227
|
...generatedMcpTools(options.schema, options.generatedTools),
|
|
2129
2228
|
...generatedSearchMcpTools(options),
|
|
2130
2229
|
...(options.tools ?? [])
|
|
2131
2230
|
];
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* Every scope the app connector's tools and resources answer to, given the
|
|
2234
|
+
* options its `createMcpHandler` gets: what a machine credential for it may
|
|
2235
|
+
* ask for (`RaybaseMcpScopeOptions.appScopes`).
|
|
2236
|
+
*/
|
|
2237
|
+
export function mcpAppConnectorScopes(options) {
|
|
2238
|
+
const scopes = new Set();
|
|
2239
|
+
for (const tool of connectorTools(options))
|
|
2240
|
+
for (const scope of requiredScopesForTool(tool))
|
|
2241
|
+
scopes.add(scope);
|
|
2242
|
+
for (const resource of options.resources ?? [])
|
|
2243
|
+
for (const scope of requiredScopesForResource(resource))
|
|
2244
|
+
scopes.add(scope);
|
|
2245
|
+
return [...scopes].filter((scope) => !isOperatorScope(scope)).sort();
|
|
2246
|
+
}
|
|
2247
|
+
export function createMcpHandler(options) {
|
|
2248
|
+
const surface = options.surface ?? "app";
|
|
2249
|
+
const secretColumns = secretColumnKeys(options.schema, options.systemTables);
|
|
2250
|
+
const allTools = connectorTools(options);
|
|
2132
2251
|
const resources = options.resources ?? [];
|
|
2133
2252
|
for (const resource of resources)
|
|
2134
2253
|
validateMcpAppResource(resource);
|
|
@@ -2216,7 +2335,12 @@ export function createMcpHandler(options) {
|
|
|
2216
2335
|
const client = db.for({
|
|
2217
2336
|
user: token.user,
|
|
2218
2337
|
source: { surface: "mcp", requestId: request.headers.get("cf-ray") ?? undefined },
|
|
2219
|
-
audit: (entry) =>
|
|
2338
|
+
audit: (entry, auditDb) => recordAudit(auditDb, {
|
|
2339
|
+
actor: mcpGrantAuditActor({ hostAccount: token.hostAccount, subjectId: token.subjectId, userId: token.user.id }),
|
|
2340
|
+
action: entry.action,
|
|
2341
|
+
target: entry.target,
|
|
2342
|
+
metadata: { ...asRecord(entry.metadata), ...mcpContext.auditMetadata }
|
|
2343
|
+
}),
|
|
2220
2344
|
notify: realtimeEnabled
|
|
2221
2345
|
? async (change) => {
|
|
2222
2346
|
if (!("seq" in change))
|
|
@@ -2367,9 +2491,21 @@ async function handleRpc(rpc, tools, resources, ctx, control, surface, mcpAppUiE
|
|
|
2367
2491
|
}
|
|
2368
2492
|
return rpcError(rpc.id, -32001, `Token scope does not allow tool: ${name}`);
|
|
2369
2493
|
}
|
|
2370
|
-
|
|
2494
|
+
const parsed = parseToolArgs(tool, params.arguments ?? {});
|
|
2495
|
+
if (!parsed.ok) {
|
|
2496
|
+
// Arguments that fail the tool's schema are a tool execution error
|
|
2497
|
+
// (MCP 2025-06-18, "Error Handling"): the model reads which fields
|
|
2498
|
+
// are wrong and calls again. A JSON-RPC error would stop it there.
|
|
2499
|
+
if (surface === "operator" && hasOperatorScope(tool.scopes)) {
|
|
2500
|
+
const metadata = operatorErrorAuditMetadata(ctx, secretColumns, name, params.arguments ?? {}, new Error(invalidToolArgumentsMessage(name, parsed.issues)));
|
|
2501
|
+
await (recordOperatorAudit
|
|
2502
|
+
? recordOperatorAudit("operator_mcp.tool.error", "_mcp_grants", ctx.mcp.grantId, metadata)
|
|
2503
|
+
: audit(control, ctx.user.id, "operator_mcp.tool.error", "_mcp_grants", ctx.mcp.grantId, metadata)).catch(() => { });
|
|
2504
|
+
}
|
|
2505
|
+
return rpcResult(rpc.id, invalidToolArgumentsResult(name, parsed.issues));
|
|
2506
|
+
}
|
|
2507
|
+
const args = parsed.value;
|
|
2371
2508
|
try {
|
|
2372
|
-
args = parseToolArgs(tool, params.arguments ?? {});
|
|
2373
2509
|
if (recordEvent) {
|
|
2374
2510
|
await recordEvent("tool.call", name, { arguments: auditArguments(secretColumns, args) });
|
|
2375
2511
|
}
|