@softure-ai/mcp-access 0.2.6 → 0.2.9
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/CHANGELOG.md +45 -0
- package/README.md +141 -10
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/messages/en.d.ts +10 -1
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +10 -1
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/index.d.ts +10 -1
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/next/oauth-page.d.ts +6 -1
- package/dist/next/oauth-page.d.ts.map +1 -1
- package/dist/next/oauth-page.js +6 -5
- package/dist/next/oauth-page.js.map +1 -1
- package/dist/server/endpoint.d.ts.map +1 -1
- package/dist/server/endpoint.js +6 -4
- package/dist/server/endpoint.js.map +1 -1
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +5 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/oauth-http.d.ts +5 -0
- package/dist/server/oauth-http.d.ts.map +1 -1
- package/dist/server/oauth-http.js +11 -7
- package/dist/server/oauth-http.js.map +1 -1
- package/dist/server/server-factory.d.ts +31 -0
- package/dist/server/server-factory.d.ts.map +1 -0
- package/dist/server/server-factory.js +49 -0
- package/dist/server/server-factory.js.map +1 -0
- package/dist/server/server-info.d.ts +34 -0
- package/dist/server/server-info.d.ts.map +1 -0
- package/dist/server/server-info.js +30 -0
- package/dist/server/server-info.js.map +1 -0
- package/dist/server/strict-input.d.ts +17 -0
- package/dist/server/strict-input.d.ts.map +1 -0
- package/dist/server/strict-input.js +157 -0
- package/dist/server/strict-input.js.map +1 -0
- package/dist/server/tool-list.d.ts +35 -0
- package/dist/server/tool-list.d.ts.map +1 -0
- package/dist/server/tool-list.js +68 -0
- package/dist/server/tool-list.js.map +1 -0
- package/dist/server/writes-switch.d.ts +8 -0
- package/dist/server/writes-switch.d.ts.map +1 -0
- package/dist/server/writes-switch.js +25 -0
- package/dist/server/writes-switch.js.map +1 -0
- package/dist/stdio/serve.d.ts.map +1 -1
- package/dist/stdio/serve.js +5 -7
- package/dist/stdio/serve.js.map +1 -1
- package/dist/testing/catalog.d.ts +8 -0
- package/dist/testing/catalog.d.ts.map +1 -1
- package/dist/testing/catalog.js +8 -19
- package/dist/testing/catalog.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/ui/consent-form.d.ts +2 -2
- package/dist/ui/consent-form.d.ts.map +1 -1
- package/dist/ui/consent-form.js +3 -2
- package/dist/ui/consent-form.js.map +1 -1
- package/module.json +2 -2
- package/package.json +1 -1
- package/src/index.ts +9 -2
- package/src/messages/en.ts +11 -1
- package/src/next/oauth-page.tsx +12 -6
- package/src/server/endpoint.ts +6 -4
- package/src/server/index.ts +23 -0
- package/src/server/oauth-http.ts +16 -7
- package/src/server/server-factory.ts +78 -0
- package/src/server/server-info.ts +76 -0
- package/src/server/strict-input.ts +191 -0
- package/src/server/tool-list.ts +105 -0
- package/src/server/writes-switch.ts +24 -0
- package/src/stdio/serve.ts +5 -7
- package/src/testing/catalog.ts +14 -23
- package/src/testing/index.ts +7 -1
- package/src/ui/consent-form.tsx +12 -8
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The server version that follows the tool list. MCP clients keep a cached `tools/list` until the
|
|
2
|
+
// version they read at `initialize` changes, so a release that adds a tool or changes a schema under
|
|
3
|
+
// the same version leaves the assistant on the old schemas. `measureToolList` fingerprints the list
|
|
4
|
+
// as the client reads it, `<release>-tools-<12 hex>`: a semver prerelease, since comparisons ignore
|
|
5
|
+
// build metadata. The same registrations always give the same version.
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { createMcpHandler, McpServer, METHOD_NOT_FOUND } from "@modelcontextprotocol/server";
|
|
8
|
+
/** The release part when none is given. */
|
|
9
|
+
export const DEFAULT_TOOL_LIST_RELEASE = "1.0.0";
|
|
10
|
+
/** Hex digits of the fingerprint in the version. */
|
|
11
|
+
const FINGERPRINT_LENGTH = 12;
|
|
12
|
+
const RELEASE_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
13
|
+
/**
|
|
14
|
+
* Registers the tools on a recording server shaped like the served one, lists them the way the endpoint
|
|
15
|
+
* serves them and returns the version with the tool count. Any change of a tool's name, title,
|
|
16
|
+
* description, annotations or input or output schema changes the version; the order of registration does not.
|
|
17
|
+
*/
|
|
18
|
+
export async function measureToolList(registerTools, options = {}) {
|
|
19
|
+
const release = options.release ?? DEFAULT_TOOL_LIST_RELEASE;
|
|
20
|
+
assertRelease(release);
|
|
21
|
+
const server = new McpServer({ name: options.name ?? "tool-list", version: release }, options.serverOptions);
|
|
22
|
+
await registerTools(server);
|
|
23
|
+
return describeToolList(await listServedTools(() => server), release);
|
|
24
|
+
}
|
|
25
|
+
/** The version and count for a listed tool set. */
|
|
26
|
+
export function describeToolList(tools, release = DEFAULT_TOOL_LIST_RELEASE) {
|
|
27
|
+
assertRelease(release);
|
|
28
|
+
return { toolListVersion: `${release}-tools-${fingerprintToolList(tools)}`, toolCount: tools.length };
|
|
29
|
+
}
|
|
30
|
+
/** The first 12 hex digits of a sha256 over the tools in name order, each as canonical JSON (keys sorted at every level). */
|
|
31
|
+
export function fingerprintToolList(tools) {
|
|
32
|
+
const ordered = [...tools].sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
33
|
+
return createHash("sha256").update(toCanonicalJson(ordered)).digest("hex").slice(0, FINGERPRINT_LENGTH);
|
|
34
|
+
}
|
|
35
|
+
/** Every tool a fresh server from the factory lists (none when it registered none), asked with one stateless `tools/list` as the endpoint serves it. */
|
|
36
|
+
export async function listServedTools(createServer) {
|
|
37
|
+
const handler = createMcpHandler(createServer, { responseMode: "json" });
|
|
38
|
+
const response = await handler.fetch(new Request("http://localhost/api/mcp", {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
|
|
41
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
|
|
42
|
+
}));
|
|
43
|
+
const answer = readAnswer(await response.text());
|
|
44
|
+
// A server that registered no tool declares no tools capability, so it does not know the method.
|
|
45
|
+
if (answer.error?.code === METHOD_NOT_FOUND)
|
|
46
|
+
return [];
|
|
47
|
+
if (answer.result?.tools === undefined) {
|
|
48
|
+
throw new Error(`@softure-ai/mcp-access: listing the server's tools failed: ${typeof answer.error?.message === "string" ? answer.error.message : `HTTP ${String(response.status)}`}`);
|
|
49
|
+
}
|
|
50
|
+
return answer.result.tools.filter((tool) => typeof tool.name === "string" && tool.name !== "");
|
|
51
|
+
}
|
|
52
|
+
/** The JSON-RPC answer, sent as JSON or as one SSE `message` event. */
|
|
53
|
+
function readAnswer(body) {
|
|
54
|
+
const data = body.startsWith("event:") ? (body.split("\n").find((line) => line.startsWith("data: "))?.slice("data: ".length) ?? "null") : body;
|
|
55
|
+
return (JSON.parse(data) ?? {});
|
|
56
|
+
}
|
|
57
|
+
function toCanonicalJson(value) {
|
|
58
|
+
return JSON.stringify(value, (_key, entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry)
|
|
59
|
+
? Object.fromEntries(Object.entries(entry).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)))
|
|
60
|
+
: entry);
|
|
61
|
+
}
|
|
62
|
+
/** A release that is not `MAJOR.MINOR.PATCH` would make an invalid version: a setup bug, thrown. */
|
|
63
|
+
function assertRelease(release) {
|
|
64
|
+
if (!RELEASE_PATTERN.test(release)) {
|
|
65
|
+
throw new Error(`@softure-ai/mcp-access: the tool list release must be MAJOR.MINOR.PATCH, got "${release}"`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=tool-list.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-list.js","sourceRoot":"","sources":["../../src/server/tool-list.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { McpAccessContext } from "./tokens.js";
|
|
2
|
+
/**
|
|
3
|
+
* Whether the app lets anyone write right now: `allowWrites` and the switch (on when the app does not
|
|
4
|
+
* define it). Without `allowWrites` the switch is not read. A read that throws counts as off (writes
|
|
5
|
+
* are the risky side) and is logged by the switch's name.
|
|
6
|
+
*/
|
|
7
|
+
export declare function isWritingEnabled(ctx: McpAccessContext): Promise<boolean>;
|
|
8
|
+
//# sourceMappingURL=writes-switch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writes-switch.d.ts","sourceRoot":"","sources":["../../src/server/writes-switch.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAS9E"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The runtime kill switch for writes. `mcp-access.writes_enabled` is read through the switch-reader
|
|
2
|
+
// contract of `@softure-ai/core` on every request: when the app defines it in
|
|
3
|
+
// `@softure-ai/feature-switches`, turning it off takes writes away at once, without a deploy. An app that
|
|
4
|
+
// does not define it keeps `allowWrites` alone.
|
|
5
|
+
import { errorLogLabel, readSwitch } from "@softure-ai/core";
|
|
6
|
+
import { WRITES_ENABLED_SWITCH } from "../index.js";
|
|
7
|
+
import { getMcpAccessOptions } from "./options.js";
|
|
8
|
+
/**
|
|
9
|
+
* Whether the app lets anyone write right now: `allowWrites` and the switch (on when the app does not
|
|
10
|
+
* define it). Without `allowWrites` the switch is not read. A read that throws counts as off (writes
|
|
11
|
+
* are the risky side) and is logged by the switch's name.
|
|
12
|
+
*/
|
|
13
|
+
export async function isWritingEnabled(ctx) {
|
|
14
|
+
if (!getMcpAccessOptions(ctx.config).allowWrites)
|
|
15
|
+
return false;
|
|
16
|
+
try {
|
|
17
|
+
const reading = await readSwitch(ctx, WRITES_ENABLED_SWITCH);
|
|
18
|
+
return reading.kind === "undeclared" || reading.isEnabled;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error(`@softure-ai/mcp-access: reading the ${WRITES_ENABLED_SWITCH} switch failed, writes are off: ${errorLogLabel(error)}`);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=writes-switch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"writes-switch.js","sourceRoot":"","sources":["../../src/server/writes-switch.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,8EAA8E;AAC9E,0GAA0G;AAC1G,gDAAgD;AAChD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAGnD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,GAAqB;IAC1D,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,WAAW;QAAE,OAAO,KAAK,CAAC;IAC/D,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC;QAC7D,OAAO,OAAO,CAAC,IAAI,KAAK,YAAY,IAAI,OAAO,CAAC,SAAS,CAAC;IAC5D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,uCAAuC,qBAAqB,mCAAmC,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACrI,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/stdio/serve.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAG9D,OAAO,
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/stdio/serve.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAG9D,OAAO,EAA8B,KAAK,aAAa,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAC9G,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG9D,sFAAsF;AACtF,eAAO,MAAM,uBAAuB,gBAAgB,CAAC;AAErD,6EAA6E;AAC7E,eAAO,MAAM,cAAc,UAAU,CAAC;AAEtC,MAAM,MAAM,iBAAiB,GAAG,8BAA8B,GAAG,6BAA6B,GAAG,oCAAoC,GAAG,kCAAkC,CAAC;AAE3K,8GAA8G;AAC9G,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,cAAc;IAC7B,8BAA8B;IAC9B,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,8FAA8F;IAC9F,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAED,MAAM,MAAM,cAAc,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAA;CAAE,CAAC;AAE3I,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAC;IACxC,8EAA8E;IAC9E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,gGAAgG;IAChG,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,8BAA8B;IAC9B,QAAQ,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC5D,iDAAiD;IACjD,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAC/B,oFAAoF;IACpF,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,qBAAqB,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;CACvF;AAED,uGAAuG;AACvG,wBAAsB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAkD1F"}
|
package/dist/stdio/serve.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { serveStdio, StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
2
2
|
import { resolveOnlyAccount } from "@softure-ai/auth/server";
|
|
3
|
-
import { errorLogLabel } from "@softure-ai/core";
|
|
3
|
+
import { errorLogLabel, systemClock } from "@softure-ai/core";
|
|
4
4
|
import { openCommandDatabase } from "@softure-ai/db";
|
|
5
|
-
import {
|
|
5
|
+
import { isWritingEnabled } from "../server/writes-switch.js";
|
|
6
6
|
/** The variable naming the account to serve, unless the entry is told another one. */
|
|
7
7
|
export const DEFAULT_MCP_USER_ID_ENV = "MCP_USER_ID";
|
|
8
8
|
/** The token id the factory receives over stdio, where there is no token. */
|
|
@@ -27,11 +27,9 @@ export async function serveMcpStdio(options) {
|
|
|
27
27
|
await database.close();
|
|
28
28
|
return { ok: false, error: userId };
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
tokenId: STDIO_TOKEN_ID,
|
|
34
|
-
};
|
|
30
|
+
// The writes switch is read once, at start: a stdio server serves one local session.
|
|
31
|
+
const canWrite = (options.allowWrites ?? false) && (await isWritingEnabled({ db: database.handle.db, clock: systemClock, config }));
|
|
32
|
+
const identity = { userId, canWrite, tokenId: STDIO_TOKEN_ID };
|
|
35
33
|
let closed = false;
|
|
36
34
|
const closeDatabase = async () => {
|
|
37
35
|
if (closed)
|
package/dist/stdio/serve.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../../src/stdio/serve.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAkD,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.js","sourceRoot":"","sources":["../../src/stdio/serve.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SoftureConfig } from "@softure-ai/core";
|
|
2
2
|
import type { McpServerFactory } from "../server/endpoint.js";
|
|
3
|
+
import { type ToolListMeasurement } from "../server/tool-list.js";
|
|
3
4
|
export interface ToolCatalogDifferences {
|
|
4
5
|
/** Configured tools the server does not register for the identity that should get them. */
|
|
5
6
|
readonly missing: readonly string[];
|
|
@@ -8,8 +9,15 @@ export interface ToolCatalogDifferences {
|
|
|
8
9
|
/** Configured write tools the server gives a read-only identity. */
|
|
9
10
|
readonly writeForReaders: readonly string[];
|
|
10
11
|
}
|
|
12
|
+
/** What `measureServedToolLists` reports: a read-only and a write identity. */
|
|
13
|
+
export interface ServedToolLists {
|
|
14
|
+
readonly read: ToolListMeasurement;
|
|
15
|
+
readonly write: ToolListMeasurement;
|
|
16
|
+
}
|
|
11
17
|
/** Every difference between the configured catalog and the server, each list in the catalog's order, then the server's. */
|
|
12
18
|
export declare function getToolCatalogDifferences(config: SoftureConfig, createServer: McpServerFactory): Promise<ToolCatalogDifferences>;
|
|
13
19
|
/** Throws a list of every difference, e.g. in a unit test next to the server. */
|
|
14
20
|
export declare function expectToolCatalogMatchesServer(config: SoftureConfig, createServer: McpServerFactory): Promise<void>;
|
|
21
|
+
/** The tool list's version and count for each access level, for a snapshot that changes whenever a tool's schema does. */
|
|
22
|
+
export declare function measureServedToolLists(createServer: McpServerFactory, release?: string): Promise<ServedToolLists>;
|
|
15
23
|
//# sourceMappingURL=catalog.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/testing/catalog.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/testing/catalog.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAE9D,OAAO,EAAqC,KAAK,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAErG,MAAM,WAAW,sBAAsB;IACrC,2FAA2F;IAC3F,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,oEAAoE;IACpE,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAED,+EAA+E;AAC/E,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAC;CACrC;AAKD,2HAA2H;AAC3H,wBAAsB,yBAAyB,CAAC,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAUtI;AAED,iFAAiF;AACjF,wBAAsB,8BAA8B,CAAC,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAUzH;AAOD,0HAA0H;AAC1H,wBAAsB,sBAAsB,CAAC,YAAY,EAAE,gBAAgB,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAKvH"}
|
package/dist/testing/catalog.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
// Catalog parity: the tools listed in softure.config.ts (the token page shows them, agent docs read
|
|
2
|
-
// them) against the tools the app's MCP server really registers, for a read-only and a write identity.
|
|
3
|
-
// Framework-free: `expectToolCatalogMatchesServer` throws, so it works in any test runner.
|
|
4
|
-
import { createMcpHandler } from "@modelcontextprotocol/server";
|
|
5
1
|
import { getMcpAccessOptions } from "../server/options.js";
|
|
2
|
+
import { describeToolList, listServedTools } from "../server/tool-list.js";
|
|
6
3
|
const READER = { userId: "00000000-0000-4000-8000-000000000001", canWrite: false, tokenId: "catalog-check" };
|
|
7
4
|
const WRITER = { ...READER, canWrite: true };
|
|
8
5
|
/** Every difference between the configured catalog and the server, each list in the catalog's order, then the server's. */
|
|
@@ -31,21 +28,13 @@ export async function expectToolCatalogMatchesServer(config, createServer) {
|
|
|
31
28
|
}
|
|
32
29
|
/** The tool names a fresh server lists for the identity, asked the way the endpoint serves it. */
|
|
33
30
|
async function listServerTools(createServer, identity) {
|
|
34
|
-
|
|
35
|
-
const response = await handler.fetch(new Request("http://localhost/api/mcp", {
|
|
36
|
-
method: "POST",
|
|
37
|
-
headers: { "content-type": "application/json", accept: "application/json, text/event-stream" },
|
|
38
|
-
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
|
|
39
|
-
}));
|
|
40
|
-
const answer = readAnswer(await response.text());
|
|
41
|
-
if (answer.result?.tools === undefined) {
|
|
42
|
-
throw new Error(`@softure-ai/mcp-access: listing the server's tools failed: ${typeof answer.error?.message === "string" ? answer.error.message : `HTTP ${String(response.status)}`}`);
|
|
43
|
-
}
|
|
44
|
-
return answer.result.tools.map((tool) => (typeof tool.name === "string" ? tool.name : "")).filter((name) => name !== "");
|
|
31
|
+
return (await listServedTools(() => createServer(identity))).map((tool) => tool.name);
|
|
45
32
|
}
|
|
46
|
-
/** The
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
33
|
+
/** The tool list's version and count for each access level, for a snapshot that changes whenever a tool's schema does. */
|
|
34
|
+
export async function measureServedToolLists(createServer, release) {
|
|
35
|
+
return {
|
|
36
|
+
read: describeToolList(await listServedTools(() => createServer(READER)), release),
|
|
37
|
+
write: describeToolList(await listServedTools(() => createServer(WRITER)), release),
|
|
38
|
+
};
|
|
50
39
|
}
|
|
51
40
|
//# sourceMappingURL=catalog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../src/testing/catalog.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../src/testing/catalog.ts"],"names":[],"mappings":"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"}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { expectToolCatalogMatchesServer, getToolCatalogDifferences, type ToolCatalogDifferences } from "./catalog.js";
|
|
1
|
+
export { expectToolCatalogMatchesServer, getToolCatalogDifferences, measureServedToolLists, type ServedToolLists, type ToolCatalogDifferences, } from "./catalog.js";
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,sBAAsB,EACtB,KAAK,eAAe,EACpB,KAAK,sBAAsB,GAC5B,MAAM,cAAc,CAAC"}
|
package/dist/testing/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// `@softure-ai/mcp-access/testing`: checks for an app's own tests.
|
|
2
|
-
export { expectToolCatalogMatchesServer, getToolCatalogDifferences } from "./catalog.js";
|
|
2
|
+
export { expectToolCatalogMatchesServer, getToolCatalogDifferences, measureServedToolLists, } from "./catalog.js";
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,OAAO,EACL,8BAA8B,EAC9B,yBAAyB,EACzB,sBAAsB,GAGvB,MAAM,cAAc,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ClassNames } from "@softure-ai/ui";
|
|
2
2
|
import type { McpAccessMessages } from "../messages/index.js";
|
|
3
|
-
export type ConsentFormSlot = "root" | "details" | "term" | "value" | "note" | "actions" | "text" | "link";
|
|
3
|
+
export type ConsentFormSlot = "root" | "details" | "term" | "value" | "target" | "note" | "actions" | "text" | "link";
|
|
4
4
|
export interface ConsentFormProps {
|
|
5
5
|
/** The decision route the form posts to. */
|
|
6
6
|
readonly action: string;
|
|
@@ -10,7 +10,7 @@ export interface ConsentFormProps {
|
|
|
10
10
|
/** Where the code goes: the host for http(s), the whole URI for a native scheme. */
|
|
11
11
|
readonly redirectTarget: string;
|
|
12
12
|
readonly accountEmail: string;
|
|
13
|
-
/** Offer the change checkbox: the client asked for `mcp:write` and the app allows writes. */
|
|
13
|
+
/** Offer the change checkbox: the client asked for `mcp:write` and the app allows writes right now. */
|
|
14
14
|
readonly isWriteOffered: boolean;
|
|
15
15
|
readonly messages: McpAccessMessages;
|
|
16
16
|
readonly classNames?: ClassNames<ConsentFormSlot>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consent-form.d.ts","sourceRoot":"","sources":["../../src/ui/consent-form.tsx"],"names":[],"mappings":"AACA,OAAO,EAAoB,KAAK,UAAU,EAAyB,MAAM,gBAAgB,CAAC;AAC1F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAQ9D,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"consent-form.d.ts","sourceRoot":"","sources":["../../src/ui/consent-form.tsx"],"names":[],"mappings":"AACA,OAAO,EAAoB,KAAK,UAAU,EAAyB,MAAM,gBAAgB,CAAC;AAC1F,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAQ9D,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAEtH,MAAM,WAAW,gBAAgB;IAC/B,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC;IACxD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,oFAAoF;IACpF,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,uGAAuG;IACvG,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,eAAe,CAAC,CAAC;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAcD,wBAAgB,WAAW,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,gBAAgB,+BAsCzJ;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,wGAAwG;IACxG,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAC;IACrC,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,eAAe,CAAC,CAAC;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,yGAAyG;AACzG,wBAAgB,YAAY,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,iBAAiB,+BAepH"}
|
package/dist/ui/consent-form.js
CHANGED
|
@@ -6,7 +6,8 @@ const DEFAULT_CLASSES = {
|
|
|
6
6
|
details: "sft:m-0 sft:flex sft:flex-col sft:gap-3 sft:text-sm",
|
|
7
7
|
term: "sft:text-muted",
|
|
8
8
|
value: "sft:m-0 sft:font-medium sft:text-foreground",
|
|
9
|
-
|
|
9
|
+
target: "sft:m-0 sft:font-mono sft:font-medium sft:text-foreground sft:break-words",
|
|
10
|
+
note: "sft:m-0 sft:mt-1 sft:text-xs sft:font-normal sft:text-muted",
|
|
10
11
|
actions: "sft:flex sft:flex-col sft:gap-3",
|
|
11
12
|
text: "sft:m-0 sft:text-sm sft:text-muted",
|
|
12
13
|
link: "sft:font-medium sft:text-accent",
|
|
@@ -14,7 +15,7 @@ const DEFAULT_CLASSES = {
|
|
|
14
15
|
export function ConsentForm({ action, params, clientName, redirectTarget, accountEmail, isWriteOffered, messages, classNames, unstyled }) {
|
|
15
16
|
const slot = createSlotClassGetter({ defaults: DEFAULT_CLASSES, classNames, unstyled });
|
|
16
17
|
const copy = messages.consent;
|
|
17
|
-
return (_jsxs("form", { method: "post", action: action, className: slot("root"), children: [params.map(([name, value]) => (_jsx("input", { type: "hidden", name: name, value: value }, name))), _jsxs("dl", { className: slot("details"), children: [_jsxs("div", { children: [_jsx("dt", { className: slot("term"), children: copy.app }),
|
|
18
|
+
return (_jsxs("form", { method: "post", action: action, className: slot("root"), children: [params.map(([name, value]) => (_jsx("input", { type: "hidden", name: name, value: value }, name))), _jsxs("dl", { className: slot("details"), children: [_jsxs("div", { children: [_jsx("dt", { className: slot("term"), children: copy.app }), _jsx("dd", { className: slot("value"), children: clientName })] }), _jsxs("div", { children: [_jsx("dt", { className: slot("term"), children: copy.redirectLabel }), _jsx("dd", { className: slot("target"), children: redirectTarget }), _jsx("dd", { className: slot("note"), children: copy.redirectHint })] }), _jsxs("div", { children: [_jsx("dt", { className: slot("term"), children: copy.account }), _jsx("dd", { className: slot("value"), children: accountEmail })] }), _jsxs("div", { children: [_jsx("dt", { className: slot("term"), children: copy.access }), _jsx("dd", { className: slot("value"), children: copy.readAccess })] })] }), isWriteOffered ? _jsx(Checkbox, { name: "canWrite", label: copy.allowWrite, description: copy.allowWriteHint, unstyled: unstyled }) : null, _jsxs("div", { className: slot("actions"), children: [_jsx(Button, { type: "submit", name: "decision", value: "allow", variant: "primary", size: "lg", fullWidth: true, unstyled: unstyled, children: copy.allow }), _jsx(Button, { type: "submit", name: "decision", value: "deny", variant: "secondary", size: "lg", fullWidth: true, unstyled: unstyled, children: copy.deny })] })] }));
|
|
18
19
|
}
|
|
19
20
|
/** Why the request cannot go on. Nothing was granted; going back to the client is the person's click. */
|
|
20
21
|
export function ConsentError({ message, backLocation, backTarget, messages, classNames, unstyled }) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"consent-form.js","sourceRoot":"","sources":["../../src/ui/consent-form.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAmB,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AA2B1F,MAAM,eAAe,GAA8C;IACjE,IAAI,EAAE,+CAA+C;IACrD,OAAO,EAAE,qDAAqD;IAC9D,IAAI,EAAE,gBAAgB;IACtB,KAAK,EAAE,6CAA6C;IACpD,IAAI,EAAE,
|
|
1
|
+
{"version":3,"file":"consent-form.js","sourceRoot":"","sources":["../../src/ui/consent-form.tsx"],"names":[],"mappings":"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"}
|
package/module.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "mcp-access",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"dependsOn": { "security": "^0.2.0", "auth": "^0.2.0", "jobs": "^0.1.1?", "ops": "^0.2.0?" },
|
|
5
5
|
"dbSchema": "mcp",
|
|
6
6
|
"tables": ["access_tokens", "oauth_clients", "oauth_authorization_codes", "oauth_grants", "oauth_registration_window"],
|
|
7
7
|
"env": [],
|
|
8
|
-
"switches": [],
|
|
8
|
+
"switches": ["mcp-access.writes_enabled"],
|
|
9
9
|
"routes": {
|
|
10
10
|
"page": "/account/mcp",
|
|
11
11
|
"endpoint": "/api/mcp",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softure-ai/mcp-access",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "SOFTURE AI MCP access module: hashed, scoped, expiring access tokens, a rate-limited Bearer endpoint around the app's MCP server factory, and a token page with client setup instructions for Next.js; tool result helpers with a safe error boundary, a stdio entry, a catalog parity check and the softure-mcp prune command.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,13 @@ import { mcpAccessPrivacyContributor } from "./server/privacy.js";
|
|
|
12
12
|
|
|
13
13
|
export const MODULE_ID = "mcp-access";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Name of the runtime switch that turns writes off without a deploy. Defined in
|
|
17
|
+
* `@softure-ai/feature-switches`, writes need it on as well as `allowWrites` and the token's write access;
|
|
18
|
+
* not defined, `allowWrites` alone decides.
|
|
19
|
+
*/
|
|
20
|
+
export const WRITES_ENABLED_SWITCH = "mcp-access.writes_enabled";
|
|
21
|
+
|
|
15
22
|
/**
|
|
16
23
|
* The rate limit bucket of `POST /api/mcp`, to spread into `security({ buckets })`: requests per
|
|
17
24
|
* client address, counted before the token is verified. Every request counts, so the limit is for
|
|
@@ -34,12 +41,12 @@ export const MCP_RATE_LIMIT_BUCKETS = {
|
|
|
34
41
|
export const mcpAccess = defineModule({
|
|
35
42
|
manifest: {
|
|
36
43
|
id: MODULE_ID,
|
|
37
|
-
version: "0.2.
|
|
44
|
+
version: "0.2.9",
|
|
38
45
|
dependsOn: { security: "^0.2.0", auth: "^0.2.0", jobs: "^0.1.1?", ops: "^0.2.0?" },
|
|
39
46
|
dbSchema: "mcp",
|
|
40
47
|
tables: ["access_tokens", "oauth_clients", "oauth_authorization_codes", "oauth_grants", "oauth_registration_window"],
|
|
41
48
|
env: [],
|
|
42
|
-
switches: [],
|
|
49
|
+
switches: [WRITES_ENABLED_SWITCH],
|
|
43
50
|
routes: { ...DEFAULT_MCP_ACCESS_ROUTES },
|
|
44
51
|
mount: [
|
|
45
52
|
{ kind: "page", path: "app/account/mcp/page.tsx", export: "McpAccessPage" },
|
package/src/messages/en.ts
CHANGED
|
@@ -70,7 +70,8 @@ export const en = {
|
|
|
70
70
|
title: "{client} wants access to your account",
|
|
71
71
|
lead: "The assistant can answer questions with your data in this app. You can disconnect it at any time on the AI assistant access page.",
|
|
72
72
|
app: "App",
|
|
73
|
-
|
|
73
|
+
redirectLabel: "Access code goes to",
|
|
74
|
+
redirectHint: "The app chose its own name; this address is the one part it cannot fake. Allow only if you recognise it.",
|
|
74
75
|
account: "Account",
|
|
75
76
|
access: "Access",
|
|
76
77
|
readAccess: "Read your data",
|
|
@@ -85,6 +86,15 @@ export const en = {
|
|
|
85
86
|
nothingGranted: "No access was granted.",
|
|
86
87
|
backToApp: "Back to the app ({target})",
|
|
87
88
|
},
|
|
89
|
+
toolInput: {
|
|
90
|
+
unknownFields: "Unknown fields: {fields}. Accepted fields: {accepted}.",
|
|
91
|
+
noFields: "none",
|
|
92
|
+
fieldRenamed: "input.field_renamed: the field \"{field}\" is now \"{renamed}\". Send it as \"{renamed}\".",
|
|
93
|
+
},
|
|
94
|
+
serverInfoTool: {
|
|
95
|
+
description:
|
|
96
|
+
"Tells which build of this server answers: its name and version (the version changes whenever a tool changes), the release, the commit, when it was built and started, and the changelog when the app publishes one. Use it to check that a change is live.",
|
|
97
|
+
},
|
|
88
98
|
errors: {
|
|
89
99
|
"mcp-access": {
|
|
90
100
|
name_required: "Give the token a name, so you can tell it from the others.",
|
package/src/next/oauth-page.tsx
CHANGED
|
@@ -7,23 +7,27 @@
|
|
|
7
7
|
import { requireUser } from "@softure-ai/auth/next";
|
|
8
8
|
import { formatMessage } from "@softure-ai/core";
|
|
9
9
|
import { getSoftureConfig } from "@softure-ai/core/next";
|
|
10
|
-
import { Card } from "@softure-ai/ui";
|
|
10
|
+
import { Card, type ClassNames } from "@softure-ai/ui";
|
|
11
11
|
import { notFound } from "next/navigation";
|
|
12
12
|
import { getConsentPath, isOAuthEnabled, readAuthorizationParams, validateAuthorizationRequest } from "../server/oauth-http.js";
|
|
13
13
|
import { describeRedirectUri } from "../server/oauth-validation.js";
|
|
14
|
-
import { getMcpAccessMessages,
|
|
15
|
-
import { ConsentError, ConsentForm } from "../ui/consent-form.js";
|
|
14
|
+
import { getMcpAccessMessages, getMcpAccessRoutes } from "../server/options.js";
|
|
15
|
+
import { ConsentError, ConsentForm, type ConsentFormSlot } from "../ui/consent-form.js";
|
|
16
16
|
import { getMcpAccessContext, getRequestOrigins } from "./context.js";
|
|
17
17
|
|
|
18
18
|
type SearchParams = Promise<Record<string, string | string[] | undefined>>;
|
|
19
19
|
|
|
20
20
|
export interface OAuthConsentPageProps {
|
|
21
21
|
readonly searchParams?: SearchParams;
|
|
22
|
+
/** Restyles the form or the error, passed to `ConsentForm` and `ConsentError`. */
|
|
23
|
+
readonly classNames?: ClassNames<ConsentFormSlot>;
|
|
24
|
+
readonly unstyled?: boolean;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
const LAYOUT_CLASS = "sft:mx-auto sft:box-border sft:w-full sft:sm:max-w-md sft:px-4 sft:py-4";
|
|
25
28
|
|
|
26
|
-
export async function OAuthConsentPage({ searchParams }: OAuthConsentPageProps) {
|
|
29
|
+
export async function OAuthConsentPage({ searchParams, classNames, unstyled }: OAuthConsentPageProps) {
|
|
30
|
+
const styling = { ...(classNames === undefined ? {} : { classNames }), ...(unstyled === undefined ? {} : { unstyled }) };
|
|
27
31
|
const config = getSoftureConfig();
|
|
28
32
|
if (!isOAuthEnabled(config)) notFound();
|
|
29
33
|
const query = (await searchParams) ?? {};
|
|
@@ -43,13 +47,14 @@ export async function OAuthConsentPage({ searchParams }: OAuthConsentPageProps)
|
|
|
43
47
|
message={message}
|
|
44
48
|
{...(outcome.kind === "redirect-error" ? { backLocation: outcome.location, backTarget: describeRedirectUri(outcome.redirectUri) } : {})}
|
|
45
49
|
messages={messages}
|
|
50
|
+
{...styling}
|
|
46
51
|
/>
|
|
47
52
|
</Card>
|
|
48
53
|
</main>
|
|
49
54
|
);
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
const { client, redirectUri,
|
|
57
|
+
const { client, redirectUri, isWriteOffered } = outcome.request;
|
|
53
58
|
return (
|
|
54
59
|
<main className={LAYOUT_CLASS}>
|
|
55
60
|
<Card title={formatMessage(messages.consent.title, { client: client.clientName })} subtitle={messages.consent.lead}>
|
|
@@ -59,8 +64,9 @@ export async function OAuthConsentPage({ searchParams }: OAuthConsentPageProps)
|
|
|
59
64
|
clientName={client.clientName}
|
|
60
65
|
redirectTarget={describeRedirectUri(redirectUri)}
|
|
61
66
|
accountEmail={user.email}
|
|
62
|
-
isWriteOffered={
|
|
67
|
+
isWriteOffered={isWriteOffered}
|
|
63
68
|
messages={messages}
|
|
69
|
+
{...styling}
|
|
64
70
|
/>
|
|
65
71
|
</Card>
|
|
66
72
|
</main>
|
package/src/server/endpoint.ts
CHANGED
|
@@ -16,10 +16,10 @@ import { errorLogLabel, getModule } from "@softure-ai/core";
|
|
|
16
16
|
import { consumeRateLimit, identifyClient } from "@softure-ai/security/server";
|
|
17
17
|
import type { McpServerIdentity } from "../contract.js";
|
|
18
18
|
import { getProtectedResourceMetadataUrl, isOAuthEnabled } from "./oauth-http.js";
|
|
19
|
-
import { getMcpAccessOptions } from "./options.js";
|
|
20
19
|
import { resolveMcpOrigins } from "./origins.js";
|
|
21
20
|
import { MCP_READ_SCOPE, MCP_WRITE_SCOPE } from "./scopes.js";
|
|
22
21
|
import { verifyAccessToken, type McpAccessContext } from "./tokens.js";
|
|
22
|
+
import { isWritingEnabled } from "./writes-switch.js";
|
|
23
23
|
|
|
24
24
|
export { MCP_READ_SCOPE, MCP_WRITE_SCOPE };
|
|
25
25
|
|
|
@@ -97,8 +97,10 @@ export function createMcpEndpoint({ createServer }: McpEndpointOptions): McpEndp
|
|
|
97
97
|
async function authenticate(ctx: McpAccessContext, request: Request): Promise<AuthInfo | Response> {
|
|
98
98
|
const header = request.headers.get("authorization");
|
|
99
99
|
// Clients request the challenge's scope first (MCP authorization, scope selection), so it names
|
|
100
|
-
// `mcp:write` while the app allows writes; the person's tick on
|
|
101
|
-
|
|
100
|
+
// `mcp:write` while the app allows writes (`allowWrites` and the writes switch); the person's tick on
|
|
101
|
+
// the consent page still decides. Read once per request: the switch can change between requests.
|
|
102
|
+
const isWriteAllowed = await isWritingEnabled(ctx);
|
|
103
|
+
const requiredScopes = isWriteAllowed ? [MCP_READ_SCOPE, MCP_WRITE_SCOPE] : [MCP_READ_SCOPE];
|
|
102
104
|
const challenge = isOAuthEnabled(ctx.config)
|
|
103
105
|
? { requiredScopes, resourceMetadataUrl: getProtectedResourceMetadataUrl(ctx.config, resolveMcpOrigins(ctx.config, request)) }
|
|
104
106
|
: { requiredScopes };
|
|
@@ -121,7 +123,7 @@ async function authenticate(ctx: McpAccessContext, request: Request): Promise<Au
|
|
|
121
123
|
token,
|
|
122
124
|
// The client is the token: it is what gets revoked, and its id may be logged.
|
|
123
125
|
clientId: verified.tokenId,
|
|
124
|
-
scopes: verified.canWrite ? [MCP_READ_SCOPE, MCP_WRITE_SCOPE] : [MCP_READ_SCOPE],
|
|
126
|
+
scopes: verified.canWrite && isWriteAllowed ? [MCP_READ_SCOPE, MCP_WRITE_SCOPE] : [MCP_READ_SCOPE],
|
|
125
127
|
// AuthInfo counts seconds; Date counts milliseconds.
|
|
126
128
|
expiresAt: Math.floor(verified.expiresAt.getTime() / 1000),
|
|
127
129
|
extra: { [USER_ID_CLAIM]: verified.userId },
|
package/src/server/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ export {
|
|
|
12
12
|
} from "./endpoint.js";
|
|
13
13
|
export { actionTool, toFormData, type ActionToolOptions, type FormActionHandler } from "./action-tool.js";
|
|
14
14
|
export { createMcpAccessContext } from "./context.js";
|
|
15
|
+
export { isWritingEnabled } from "./writes-switch.js";
|
|
15
16
|
export { checkAccessTokensTable } from "./health.js";
|
|
16
17
|
export {
|
|
17
18
|
closeClientRegistration,
|
|
@@ -102,6 +103,28 @@ export {
|
|
|
102
103
|
type RevokeAccessTokenInput,
|
|
103
104
|
type VerifiedAccessToken,
|
|
104
105
|
} from "./tokens.js";
|
|
106
|
+
export { createMcpServerFactory, type McpServerFactoryOptions } from "./server-factory.js";
|
|
107
|
+
export {
|
|
108
|
+
registerServerInfoTool,
|
|
109
|
+
SERVER_INFO_TOOL_NAME,
|
|
110
|
+
type McpBuildInfo,
|
|
111
|
+
type RegisterServerInfoToolOptions,
|
|
112
|
+
type ServedServerInfo,
|
|
113
|
+
type ServerInfoAnswer,
|
|
114
|
+
type ServerInfoToolOptions,
|
|
115
|
+
} from "./server-info.js";
|
|
116
|
+
export { toStrictToolInput, withStrictToolInput, type StrictToolInputMessages, type StrictToolInputOptions } from "./strict-input.js";
|
|
117
|
+
export {
|
|
118
|
+
DEFAULT_TOOL_LIST_RELEASE,
|
|
119
|
+
describeToolList,
|
|
120
|
+
fingerprintToolList,
|
|
121
|
+
listServedTools,
|
|
122
|
+
measureToolList,
|
|
123
|
+
type MeasureToolListOptions,
|
|
124
|
+
type RegisterTools,
|
|
125
|
+
type ServedTool,
|
|
126
|
+
type ToolListMeasurement,
|
|
127
|
+
} from "./tool-list.js";
|
|
105
128
|
export {
|
|
106
129
|
toolError,
|
|
107
130
|
toolResult,
|