@raybase/core 0.3.5 → 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 +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -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 +182 -45
- package/dist/mcp.js.map +1 -1
- package/dist/operator-mcp.d.ts.map +1 -1
- package/dist/operator-mcp.js +5 -4
- package/dist/operator-mcp.js.map +1 -1
- package/dist/request-context.d.ts +5 -0
- package/dist/request-context.d.ts.map +1 -0
- package/dist/request-context.js +22 -0
- package/dist/request-context.js.map +1 -0
- 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 +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- 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 +2 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +76 -43
- package/dist/worker.js.map +1 -1
- package/package.json +2 -2
package/dist/mcp.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { RAYBASE_CORE_VERSION } from "./version.js";
|
|
2
3
|
import { withHostUserRoleCeiling } from "./host-user.js";
|
|
3
4
|
import { redactSecretColumnKeys, secretColumnKeys } from "./data-secrets.js";
|
|
5
|
+
import { actorAuditRef, recordAudit } from "./audit.js";
|
|
4
6
|
import { audit, base64url, normalizeRoles } from "./auth.js";
|
|
7
|
+
import { servicePrincipal } from "./principal.js";
|
|
5
8
|
import { hasAdminAccess, normalizeStoredAdminRole } from "./admin-role.js";
|
|
6
9
|
import { inputSchemaForTable } from "./data-client.js";
|
|
7
10
|
import { createRaybaseDb, dataStub } from "./db.js";
|
|
8
11
|
import { sha256Hex } from "./internal.js";
|
|
9
|
-
import {
|
|
12
|
+
import { actionInputIssues } from "./actions.js";
|
|
13
|
+
import { jsonSchemaFor, parseWithSchema } from "./json-schema.js";
|
|
10
14
|
import { searchIndex, updateSearchForDataChange } from "./search.js";
|
|
11
15
|
import { id, longText, raybaseSystemTable, text } from "./schema.js";
|
|
12
16
|
import { randomId } from "./sql.js";
|
|
13
17
|
import { wallNowIso as nowIso, wallNowMs } from "./clock.js";
|
|
14
18
|
import { ensureManifestTables } from "./system-tables.js";
|
|
19
|
+
import { sameOriginPath } from "./same-origin-path.js";
|
|
15
20
|
export const mcpAppsExtensionId = "io.modelcontextprotocol/ui";
|
|
16
21
|
export const mcpAppResourceMimeType = "text/html;profile=mcp-app";
|
|
17
22
|
const jsonHeaders = { "content-type": "application/json; charset=utf-8" };
|
|
@@ -595,7 +600,29 @@ export function wwwAuthenticate(request, config, surface = "app") {
|
|
|
595
600
|
const scope = surface === "operator" ? "operator:read" : "mcp:read mcp:write";
|
|
596
601
|
return `Bearer resource_metadata="${serverOrigin(request)}${metadataPath}", resource="${resource}", scope="${scope}"`;
|
|
597
602
|
}
|
|
598
|
-
|
|
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
|
+
}
|
|
599
626
|
return jsonResponse({ error }, {
|
|
600
627
|
status: 401,
|
|
601
628
|
headers: { "www-authenticate": wwwAuthenticate(request, config, surface), ...Object.fromEntries(corsHeaders(request, config)) }
|
|
@@ -875,6 +902,12 @@ async function registerConfidentialClient(request, config, db, body, user, optio
|
|
|
875
902
|
return jsonResponse({ error: "invalid_client_metadata", error_description: "token_endpoint_auth_method must be client_secret_basic or client_secret_post" }, { status: 400 });
|
|
876
903
|
}
|
|
877
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
|
+
}
|
|
878
911
|
let created;
|
|
879
912
|
try {
|
|
880
913
|
created = await createMcpMachineCredential(db, config, {
|
|
@@ -944,6 +977,15 @@ export async function createMcpMachineCredential(db, config, input, options = {}
|
|
|
944
977
|
const scopeError = authorizeMcpScopes(input.user, scopes, surface, options);
|
|
945
978
|
if (scopeError)
|
|
946
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
|
+
}
|
|
947
989
|
const authMethod = input.tokenEndpointAuthMethod ?? "client_secret_basic";
|
|
948
990
|
const user = input.user;
|
|
949
991
|
const clientId = `raybase_cc_${randomId("client")}`;
|
|
@@ -1115,8 +1157,12 @@ function consentStylesheetHref(config) {
|
|
|
1115
1157
|
const href = config.mcp?.consent?.stylesheet?.trim();
|
|
1116
1158
|
if (!href)
|
|
1117
1159
|
return null;
|
|
1118
|
-
if (href.startsWith("/")
|
|
1119
|
-
|
|
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
|
+
}
|
|
1120
1166
|
try {
|
|
1121
1167
|
if (new URL(href).protocol === "https:")
|
|
1122
1168
|
return href;
|
|
@@ -1257,6 +1303,18 @@ export async function mcpAuthorizationOrganizationHint(request) {
|
|
|
1257
1303
|
const value = asString(input.org).trim();
|
|
1258
1304
|
return value || null;
|
|
1259
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
|
+
}
|
|
1260
1318
|
/** The identity a host-account grant acts as: a plain app user, never an operator. */
|
|
1261
1319
|
function hostAccountUser(hostAccount, subject) {
|
|
1262
1320
|
return {
|
|
@@ -1331,13 +1389,18 @@ async function createGrantAndCode(db, config, request, user, values, clientName,
|
|
|
1331
1389
|
addSeconds(300),
|
|
1332
1390
|
now
|
|
1333
1391
|
]);
|
|
1334
|
-
await
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
+
}
|
|
1341
1404
|
});
|
|
1342
1405
|
const redirect = new URL(values.redirect_uri);
|
|
1343
1406
|
redirect.searchParams.set("code", code);
|
|
@@ -1382,10 +1445,10 @@ function authorizeMcpScopes(user, scopes, surface, options = {}) {
|
|
|
1382
1445
|
return null;
|
|
1383
1446
|
}
|
|
1384
1447
|
function mcpSignInPath(config) {
|
|
1385
|
-
const
|
|
1386
|
-
if (!
|
|
1387
|
-
throw new Error("mcp.oauth.signInPath must be a root-relative path");
|
|
1388
|
-
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;
|
|
1389
1452
|
}
|
|
1390
1453
|
/** The authorization request as a GET address on this origin, path and query only. */
|
|
1391
1454
|
function authorizationRequestPath(values, choice, input) {
|
|
@@ -1423,10 +1486,11 @@ export async function authorizeMcpOAuthRequest(request, config, db, user, option
|
|
|
1423
1486
|
return redirectWithError(validated.values.redirect_uri, validated.values.state || null, "access_denied");
|
|
1424
1487
|
return createGrantAndCode(db, config, request, user, validated.values, validated.clientName, options, binding);
|
|
1425
1488
|
}
|
|
1426
|
-
const
|
|
1427
|
-
if (
|
|
1428
|
-
|
|
1429
|
-
|
|
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");
|
|
1430
1494
|
const target = new URL(ownPage, request.url);
|
|
1431
1495
|
target.search = new URL(request.url).search;
|
|
1432
1496
|
return new Response(null, { status: 302, headers: { location: target.toString(), "cache-control": "no-store" } });
|
|
@@ -1518,8 +1582,9 @@ export async function handleMcpHostConnect(request, config, options) {
|
|
|
1518
1582
|
/**
|
|
1519
1583
|
* The authorization endpoint when host approval is on: records a pending
|
|
1520
1584
|
* authorization for the requested host account and sends the person to the
|
|
1521
|
-
* host's admin to approve it. With no host account, sends
|
|
1522
|
-
* 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.
|
|
1523
1588
|
*/
|
|
1524
1589
|
export async function beginMcpHostAuthorization(request, config, db, env, options) {
|
|
1525
1590
|
const limited = checkMcpRateLimit(request, "host-oauth-authorize", 120);
|
|
@@ -1527,7 +1592,10 @@ export async function beginMcpHostAuthorization(request, config, db, env, option
|
|
|
1527
1592
|
return limited;
|
|
1528
1593
|
const param = hostAccountParam(options);
|
|
1529
1594
|
const input = await authorizationInput(request);
|
|
1530
|
-
|
|
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)) {
|
|
1531
1599
|
const connect = new URL(mcpHostApprovalPath(options, "connectPath"), request.url);
|
|
1532
1600
|
for (const [key, value] of Object.entries(input))
|
|
1533
1601
|
connect.searchParams.set(key, value);
|
|
@@ -1538,8 +1606,8 @@ export async function beginMcpHostAuthorization(request, config, db, env, option
|
|
|
1538
1606
|
return validated;
|
|
1539
1607
|
if (validated.surface.id !== "app")
|
|
1540
1608
|
return jsonResponse({ error: "Host-account MCP grants are only supported for the app MCP resource" }, { status: 400 });
|
|
1541
|
-
const resource = validated.values.resource ||
|
|
1542
|
-
if (resource !==
|
|
1609
|
+
const resource = validated.values.resource || canonical;
|
|
1610
|
+
if (resource !== canonical)
|
|
1543
1611
|
return redirectWithError(validated.values.redirect_uri, validated.values.state || null, "invalid_target");
|
|
1544
1612
|
const hostAccount = options.normalizeHostAccount(input[param]);
|
|
1545
1613
|
const pendingId = randomId("mcp_pending");
|
|
@@ -1827,40 +1895,40 @@ export async function validateMcpBearerToken(request, config, db, expectedSurfac
|
|
|
1827
1895
|
const header = request.headers.get("authorization") ?? "";
|
|
1828
1896
|
const match = /^Bearer\s+(.+)$/i.exec(header);
|
|
1829
1897
|
if (!match) {
|
|
1830
|
-
return unauthorizedMcpResponse(request, config, "Unauthorized", expectedSurface);
|
|
1898
|
+
return unauthorizedMcpResponse(request, config, "Unauthorized", expectedSurface, options);
|
|
1831
1899
|
}
|
|
1832
1900
|
const tokenHash = await sha256Hex(match[1]);
|
|
1833
1901
|
const row = await db.first(`SELECT * FROM "_oauth_access_tokens" WHERE "tokenHash" = ?`, [tokenHash]);
|
|
1834
1902
|
if (!row || row.revokedAt || Date.parse(row.expiresAt) <= wallNowMs()) {
|
|
1835
|
-
return unauthorizedMcpResponse(request, config, "Invalid or expired token", expectedSurface);
|
|
1903
|
+
return unauthorizedMcpResponse(request, config, "Invalid or expired token", expectedSurface, options);
|
|
1836
1904
|
}
|
|
1837
1905
|
const expectedResource = resourceOverride ?? canonicalMcpResourceForSurface(request, config, expectedSurface);
|
|
1838
1906
|
if (row.resource !== expectedResource)
|
|
1839
|
-
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);
|
|
1840
1908
|
const grant = await loadGrant(db, row.grantId);
|
|
1841
1909
|
if (!grant || grant.revokedAt || grant.clientId !== row.clientId || grant.resource !== expectedResource) {
|
|
1842
|
-
return unauthorizedMcpResponse(request, config, "MCP grant is no longer active", expectedSurface);
|
|
1910
|
+
return unauthorizedMcpResponse(request, config, "MCP grant is no longer active", expectedSurface, options);
|
|
1843
1911
|
}
|
|
1844
1912
|
if (grant.expiresAt && Date.parse(grant.expiresAt) <= wallNowMs())
|
|
1845
|
-
return unauthorizedMcpResponse(request, config, "MCP grant has expired", expectedSurface);
|
|
1913
|
+
return unauthorizedMcpResponse(request, config, "MCP grant has expired", expectedSurface, options);
|
|
1846
1914
|
const user = await grantUser(db, grant, row.userId, options);
|
|
1847
1915
|
if (!user)
|
|
1848
|
-
return unauthorizedMcpResponse(request, config, "MCP subject is no longer valid", expectedSurface);
|
|
1916
|
+
return unauthorizedMcpResponse(request, config, "MCP subject is no longer valid", expectedSurface, options);
|
|
1849
1917
|
const resourceRequest = resourceOverride ? new Request(expectedResource) : request;
|
|
1850
1918
|
const surface = mcpSurfaceForResource(resourceRequest, config, expectedResource, options);
|
|
1851
1919
|
if (!surface)
|
|
1852
|
-
return unauthorizedMcpResponse(request, config, "Unknown MCP resource", expectedSurface);
|
|
1920
|
+
return unauthorizedMcpResponse(request, config, "Unknown MCP resource", expectedSurface, options);
|
|
1853
1921
|
if (surface.id === "operator") {
|
|
1854
1922
|
if (grant.subjectType !== "raybase-operator" || grant.subjectId !== user.id) {
|
|
1855
|
-
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);
|
|
1856
1924
|
}
|
|
1857
1925
|
const grantScopes = splitScopes(parseJson(grant.scopes));
|
|
1858
1926
|
if (authorizeMcpScopes(user, grantScopes, surface, options)) {
|
|
1859
|
-
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);
|
|
1860
1928
|
}
|
|
1861
1929
|
}
|
|
1862
1930
|
else if (grant.subjectType === "raybase-operator" || row.scope.includes("operator:")) {
|
|
1863
|
-
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);
|
|
1864
1932
|
}
|
|
1865
1933
|
const tokenHostAccount = grant.hostAccount || undefined;
|
|
1866
1934
|
if (options.hostApproval) {
|
|
@@ -1920,10 +1988,44 @@ function mcpToolDescriptor(tool) {
|
|
|
1920
1988
|
_meta: mcpToolMeta(tool)
|
|
1921
1989
|
};
|
|
1922
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
|
+
*/
|
|
1923
1996
|
function parseToolArgs(tool, raw) {
|
|
1924
|
-
|
|
1925
|
-
return tool.inputSchema
|
|
1926
|
-
|
|
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
|
+
};
|
|
1927
2029
|
}
|
|
1928
2030
|
function rpcResult(id, result) {
|
|
1929
2031
|
return { jsonrpc: "2.0", id: id ?? null, result };
|
|
@@ -2120,14 +2222,32 @@ async function event(db, grantId, clientId, action, toolName, metadata) {
|
|
|
2120
2222
|
await db.run(`INSERT INTO "_mcp_grant_events" ("id", "grantId", "clientId", "action", "toolName", "metadata", "createdAt")
|
|
2121
2223
|
VALUES (?, ?, ?, ?, ?, ?, ?)`, [randomId("mcp_event"), grantId, clientId, action, toolName ?? null, metadata === undefined ? null : JSON.stringify(metadata), nowIso()]);
|
|
2122
2224
|
}
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
const secretColumns = secretColumnKeys(options.schema, options.systemTables);
|
|
2126
|
-
const allTools = [
|
|
2225
|
+
function connectorTools(options) {
|
|
2226
|
+
return [
|
|
2127
2227
|
...generatedMcpTools(options.schema, options.generatedTools),
|
|
2128
2228
|
...generatedSearchMcpTools(options),
|
|
2129
2229
|
...(options.tools ?? [])
|
|
2130
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);
|
|
2131
2251
|
const resources = options.resources ?? [];
|
|
2132
2252
|
for (const resource of resources)
|
|
2133
2253
|
validateMcpAppResource(resource);
|
|
@@ -2215,7 +2335,12 @@ export function createMcpHandler(options) {
|
|
|
2215
2335
|
const client = db.for({
|
|
2216
2336
|
user: token.user,
|
|
2217
2337
|
source: { surface: "mcp", requestId: request.headers.get("cf-ray") ?? undefined },
|
|
2218
|
-
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
|
+
}),
|
|
2219
2344
|
notify: realtimeEnabled
|
|
2220
2345
|
? async (change) => {
|
|
2221
2346
|
if (!("seq" in change))
|
|
@@ -2342,7 +2467,7 @@ async function handleRpc(rpc, tools, resources, ctx, control, surface, mcpAppUiE
|
|
|
2342
2467
|
return rpcResult(rpc.id, {
|
|
2343
2468
|
protocolVersion: "2025-06-18",
|
|
2344
2469
|
capabilities,
|
|
2345
|
-
serverInfo: { name: "raybase", title: ctx.config ? mcpConnectorName(ctx.config) : undefined, version:
|
|
2470
|
+
serverInfo: { name: "raybase", title: ctx.config ? mcpConnectorName(ctx.config) : undefined, version: RAYBASE_CORE_VERSION },
|
|
2346
2471
|
...(surface === "app" && ctx.config && mcpInstructions(ctx.config) ? { instructions: mcpInstructions(ctx.config) } : {})
|
|
2347
2472
|
});
|
|
2348
2473
|
case "ping":
|
|
@@ -2366,9 +2491,21 @@ async function handleRpc(rpc, tools, resources, ctx, control, surface, mcpAppUiE
|
|
|
2366
2491
|
}
|
|
2367
2492
|
return rpcError(rpc.id, -32001, `Token scope does not allow tool: ${name}`);
|
|
2368
2493
|
}
|
|
2369
|
-
|
|
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;
|
|
2370
2508
|
try {
|
|
2371
|
-
args = parseToolArgs(tool, params.arguments ?? {});
|
|
2372
2509
|
if (recordEvent) {
|
|
2373
2510
|
await recordEvent("tool.call", name, { arguments: auditArguments(secretColumns, args) });
|
|
2374
2511
|
}
|