@softure-ai/mcp-access 0.1.11 → 0.1.13
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 +32 -0
- package/README.md +136 -11
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +5 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/command.d.ts +9 -0
- package/dist/cli/command.d.ts.map +1 -0
- package/dist/cli/command.js +30 -0
- package/dist/cli/command.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/run.d.ts +24 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +58 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/contract.d.ts +4 -2
- package/dist/contract.d.ts.map +1 -1
- package/dist/index.d.ts +20 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -11
- package/dist/index.js.map +1 -1
- package/dist/messages/en.d.ts +2 -0
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +2 -0
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/index.d.ts +4 -0
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/messages/pl.d.ts.map +1 -1
- package/dist/messages/pl.js +2 -0
- package/dist/messages/pl.js.map +1 -1
- package/dist/next/actions.d.ts +2 -2
- package/dist/next/actions.d.ts.map +1 -1
- package/dist/next/actions.js +8 -47
- package/dist/next/actions.js.map +1 -1
- package/dist/next/format.d.ts +1 -1
- package/dist/next/format.d.ts.map +1 -1
- package/dist/next/format.js +3 -1
- package/dist/next/format.js.map +1 -1
- package/dist/next/index.d.ts +3 -2
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +2 -1
- package/dist/next/index.js.map +1 -1
- package/dist/next/issue-token.d.ts +15 -0
- package/dist/next/issue-token.d.ts.map +1 -0
- package/dist/next/issue-token.js +77 -0
- package/dist/next/issue-token.js.map +1 -0
- package/dist/next/pages.d.ts +9 -1
- package/dist/next/pages.d.ts.map +1 -1
- package/dist/next/pages.js +2 -2
- package/dist/next/pages.js.map +1 -1
- package/dist/options.d.ts +16 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js +7 -3
- package/dist/options.js.map +1 -1
- package/dist/origins.d.ts +3 -14
- package/dist/origins.d.ts.map +1 -1
- package/dist/origins.js +4 -20
- package/dist/origins.js.map +1 -1
- package/dist/routes.d.ts +15 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +17 -0
- package/dist/routes.js.map +1 -0
- package/dist/server/action-tool.d.ts +21 -0
- package/dist/server/action-tool.d.ts.map +1 -0
- package/dist/server/action-tool.js +51 -0
- package/dist/server/action-tool.js.map +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +3 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/oauth-http.js +1 -1
- package/dist/server/oauth-http.js.map +1 -1
- package/dist/server/origins.d.ts +1 -1
- package/dist/server/origins.d.ts.map +1 -1
- package/dist/server/origins.js +7 -2
- package/dist/server/origins.js.map +1 -1
- package/dist/server/prune.d.ts +8 -0
- package/dist/server/prune.d.ts.map +1 -0
- package/dist/server/prune.js +10 -0
- package/dist/server/prune.js.map +1 -0
- package/dist/server/tools.d.ts +25 -0
- package/dist/server/tools.d.ts.map +1 -0
- package/dist/server/tools.js +61 -0
- package/dist/server/tools.js.map +1 -0
- package/dist/stdio/index.d.ts +2 -0
- package/dist/stdio/index.d.ts.map +1 -0
- package/dist/stdio/index.js +4 -0
- package/dist/stdio/index.js.map +1 -0
- package/dist/stdio/register.d.ts +2 -0
- package/dist/stdio/register.d.ts.map +1 -0
- package/dist/stdio/register.js +10 -0
- package/dist/stdio/register.js.map +1 -0
- package/dist/stdio/serve.d.ts +45 -0
- package/dist/stdio/serve.d.ts.map +1 -0
- package/dist/stdio/serve.js +89 -0
- package/dist/stdio/serve.js.map +1 -0
- package/dist/testing/catalog.d.ts +15 -0
- package/dist/testing/catalog.d.ts.map +1 -0
- package/dist/testing/catalog.js +51 -0
- package/dist/testing/catalog.js.map +1 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/index.js.map +1 -0
- package/module.json +1 -1
- package/package.json +31 -5
- package/src/cli/bin.ts +5 -0
- package/src/cli/command.ts +38 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/run.ts +79 -0
- package/src/contract.ts +5 -2
- package/src/index.ts +9 -12
- package/src/messages/en.ts +2 -0
- package/src/messages/pl.ts +2 -0
- package/src/next/actions.ts +8 -45
- package/src/next/format.ts +2 -2
- package/src/next/index.ts +3 -2
- package/src/next/issue-token.ts +90 -0
- package/src/next/pages.tsx +11 -3
- package/src/options.ts +7 -3
- package/src/origins.ts +6 -27
- package/src/routes.ts +17 -0
- package/src/server/action-tool.ts +64 -0
- package/src/server/index.ts +3 -0
- package/src/server/oauth-http.ts +1 -1
- package/src/server/origins.ts +6 -5
- package/src/server/prune.ts +14 -0
- package/src/server/tools.ts +72 -0
- package/src/stdio/index.ts +12 -0
- package/src/stdio/register.ts +11 -0
- package/src/stdio/serve.ts +133 -0
- package/src/testing/catalog.ts +74 -0
- package/src/testing/index.ts +2 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { serveStdio, StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
2
|
+
import { users } from "@softure-ai/auth";
|
|
3
|
+
import { errorLogLabel } from "@softure-ai/core";
|
|
4
|
+
import { openCommandDatabase } from "@softure-ai/db";
|
|
5
|
+
import { eq } from "drizzle-orm";
|
|
6
|
+
import { getMcpAccessOptions } from "../server/options.js";
|
|
7
|
+
/** The variable naming the account to serve, unless the entry is told another one. */
|
|
8
|
+
export const DEFAULT_MCP_USER_ID_ENV = "MCP_USER_ID";
|
|
9
|
+
/** The token id the factory receives over stdio, where there is no token. */
|
|
10
|
+
export const STDIO_TOKEN_ID = "stdio";
|
|
11
|
+
const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12
|
+
/** Resolves the account, then serves the app's server for it over stdio until the client goes away. */
|
|
13
|
+
export async function serveMcpStdio(options) {
|
|
14
|
+
const { config } = options;
|
|
15
|
+
if (config.database === null)
|
|
16
|
+
return refuse("mcp-access.stdio_no_database", "softure.config.ts has no database; mcp-access needs one");
|
|
17
|
+
const userIdEnv = options.userIdEnv ?? DEFAULT_MCP_USER_ID_ENV;
|
|
18
|
+
const env = options.env ?? process.env;
|
|
19
|
+
const database = await (options.openDatabase ?? ((settings) => openCommandDatabase(settings, { max: 1 })))(config.database);
|
|
20
|
+
let userId;
|
|
21
|
+
try {
|
|
22
|
+
userId = await resolveAccount(database, env[userIdEnv]?.trim() ?? "", userIdEnv);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
await database.close();
|
|
26
|
+
throw new Error(`@softure-ai/mcp-access: looking up the account to serve failed: ${errorLogLabel(error)}`, { cause: error });
|
|
27
|
+
}
|
|
28
|
+
if (typeof userId !== "string") {
|
|
29
|
+
await database.close();
|
|
30
|
+
return { ok: false, error: userId };
|
|
31
|
+
}
|
|
32
|
+
const identity = {
|
|
33
|
+
userId,
|
|
34
|
+
canWrite: (options.allowWrites ?? false) && getMcpAccessOptions(config).allowWrites,
|
|
35
|
+
tokenId: STDIO_TOKEN_ID,
|
|
36
|
+
};
|
|
37
|
+
let closed = false;
|
|
38
|
+
const closeDatabase = async () => {
|
|
39
|
+
if (closed)
|
|
40
|
+
return;
|
|
41
|
+
closed = true;
|
|
42
|
+
await database.close();
|
|
43
|
+
};
|
|
44
|
+
const transport = options.transport ?? new StdioServerTransport();
|
|
45
|
+
const handle = serveStdio(() => options.createServer(identity), {
|
|
46
|
+
transport,
|
|
47
|
+
onerror: (error) => console.error(`@softure-ai/mcp-access: the stdio connection reported: ${errorLogLabel(error)}`),
|
|
48
|
+
});
|
|
49
|
+
// serveStdio has set its own handler by now; the database follows the connection, so a client that
|
|
50
|
+
// closes stdin leaves no open pool keeping the process alive.
|
|
51
|
+
const servedOnClose = transport.onclose;
|
|
52
|
+
transport.onclose = () => {
|
|
53
|
+
servedOnClose?.();
|
|
54
|
+
void closeDatabase();
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
ok: true,
|
|
58
|
+
value: {
|
|
59
|
+
identity,
|
|
60
|
+
close: async () => {
|
|
61
|
+
await handle.close();
|
|
62
|
+
await closeDatabase();
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** The account id, or why there is none. Never the first of several accounts: that could be another person's data. */
|
|
68
|
+
async function resolveAccount(database, requested, userIdEnv) {
|
|
69
|
+
const { db } = database.handle;
|
|
70
|
+
if (requested !== "") {
|
|
71
|
+
const unknown = { code: "mcp-access.stdio_account_unknown", message: `@softure-ai/mcp-access: ${userIdEnv} names no account` };
|
|
72
|
+
if (!UUID_SHAPE.test(requested))
|
|
73
|
+
return unknown;
|
|
74
|
+
const found = await db.select({ id: users.id }).from(users).where(eq(users.id, requested)).limit(1);
|
|
75
|
+
return found[0]?.id ?? unknown;
|
|
76
|
+
}
|
|
77
|
+
const accounts = await db.select({ id: users.id }).from(users).limit(2);
|
|
78
|
+
const [only, second] = accounts;
|
|
79
|
+
if (only === undefined)
|
|
80
|
+
return { code: "mcp-access.stdio_no_account", message: "@softure-ai/mcp-access: the database has no account to serve" };
|
|
81
|
+
if (second !== undefined) {
|
|
82
|
+
return { code: "mcp-access.stdio_account_ambiguous", message: `@softure-ai/mcp-access: the database has several accounts; set ${userIdEnv} to the id of the one to serve` };
|
|
83
|
+
}
|
|
84
|
+
return only.id;
|
|
85
|
+
}
|
|
86
|
+
function refuse(code, reason) {
|
|
87
|
+
return { ok: false, error: { code, message: `@softure-ai/mcp-access: ${reason}` } };
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=serve.js.map
|
|
@@ -0,0 +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,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,aAAa,EAAkD,MAAM,kBAAkB,CAAC;AACjG,OAAO,EAAE,mBAAmB,EAAwB,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AAGjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE3D,sFAAsF;AACtF,MAAM,CAAC,MAAM,uBAAuB,GAAG,aAAa,CAAC;AAErD,6EAA6E;AAC7E,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AAEtC,MAAM,UAAU,GAAG,iEAAiE,CAAC;AAkCrF,uGAAuG;AACvG,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAA6B;IAC/D,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAC3B,IAAI,MAAM,CAAC,QAAQ,KAAK,IAAI;QAAE,OAAO,MAAM,CAAC,8BAA8B,EAAE,yDAAyD,CAAC,CAAC;IACvI,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,uBAAuB,CAAC;IAC/D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACvC,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,mBAAmB,CAAC,QAAQ,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAE5H,IAAI,MAA8B,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,cAAc,CAAC,QAAQ,EAAE,GAAG,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,SAAS,CAAC,CAAC;IACnF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,mEAAmE,aAAa,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/H,CAAC;IACD,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC;QACvB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACtC,CAAC;IAED,MAAM,QAAQ,GAAsB;QAClC,MAAM;QACN,QAAQ,EAAE,CAAC,OAAO,CAAC,WAAW,IAAI,KAAK,CAAC,IAAI,mBAAmB,CAAC,MAAM,CAAC,CAAC,WAAW;QACnF,OAAO,EAAE,cAAc;KACxB,CAAC;IACF,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,MAAM,aAAa,GAAG,KAAK,IAAI,EAAE;QAC/B,IAAI,MAAM;YAAE,OAAO;QACnB,MAAM,GAAG,IAAI,CAAC;QACd,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC,CAAC;IACF,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,IAAI,oBAAoB,EAAE,CAAC;IAClE,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE;QAC9D,SAAS;QACT,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,0DAA0D,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC;KACpH,CAAC,CAAC;IACH,mGAAmG;IACnG,8DAA8D;IAC9D,MAAM,aAAa,GAAG,SAAS,CAAC,OAAO,CAAC;IACxC,SAAS,CAAC,OAAO,GAAG,GAAG,EAAE;QACvB,aAAa,EAAE,EAAE,CAAC;QAClB,KAAK,aAAa,EAAE,CAAC;IACvB,CAAC,CAAC;IACF,OAAO;QACL,EAAE,EAAE,IAAI;QACR,KAAK,EAAE;YACL,QAAQ;YACR,KAAK,EAAE,KAAK,IAAI,EAAE;gBAChB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;gBACrB,MAAM,aAAa,EAAE,CAAC;YACxB,CAAC;SACF;KACF,CAAC;AACJ,CAAC;AAED,sHAAsH;AACtH,KAAK,UAAU,cAAc,CAAC,QAAyB,EAAE,SAAiB,EAAE,SAAiB;IAC3F,MAAM,EAAE,EAAE,EAAE,GAAG,QAAQ,CAAC,MAAM,CAAC;IAC/B,IAAI,SAAS,KAAK,EAAE,EAAE,CAAC;QACrB,MAAM,OAAO,GAAG,EAAE,IAAI,EAAE,kCAAkC,EAAE,OAAO,EAAE,2BAA2B,SAAS,mBAAmB,EAAW,CAAC;QACxI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC;YAAE,OAAO,OAAO,CAAC;QAChD,MAAM,KAAK,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACpG,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,IAAI,OAAO,CAAC;IACjC,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACxE,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,QAAQ,CAAC;IAChC,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,6BAA6B,EAAE,OAAO,EAAE,8DAA8D,EAAE,CAAC;IAChJ,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO,EAAE,IAAI,EAAE,oCAAoC,EAAE,OAAO,EAAE,kEAAkE,SAAS,gCAAgC,EAAE,CAAC;IAC9K,CAAC;IACD,OAAO,IAAI,CAAC,EAAE,CAAC;AACjB,CAAC;AAED,SAAS,MAAM,CAAC,IAAuB,EAAE,MAAc;IACrD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,2BAA2B,MAAM,EAAE,EAAE,EAAE,CAAC;AACtF,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SoftureConfig } from "@softure-ai/core";
|
|
2
|
+
import type { McpServerFactory } from "../server/endpoint.js";
|
|
3
|
+
export interface ToolCatalogDifferences {
|
|
4
|
+
/** Configured tools the server does not register for the identity that should get them. */
|
|
5
|
+
readonly missing: readonly string[];
|
|
6
|
+
/** Tools the server registers that the catalog does not list. */
|
|
7
|
+
readonly unlisted: readonly string[];
|
|
8
|
+
/** Configured write tools the server gives a read-only identity. */
|
|
9
|
+
readonly writeForReaders: readonly string[];
|
|
10
|
+
}
|
|
11
|
+
/** Every difference between the configured catalog and the server, each list in the catalog's order, then the server's. */
|
|
12
|
+
export declare function getToolCatalogDifferences(config: SoftureConfig, createServer: McpServerFactory): Promise<ToolCatalogDifferences>;
|
|
13
|
+
/** Throws a list of every difference, e.g. in a unit test next to the server. */
|
|
14
|
+
export declare function expectToolCatalogMatchesServer(config: SoftureConfig, createServer: McpServerFactory): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/testing/catalog.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG9D,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;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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
import { getMcpAccessOptions } from "../server/options.js";
|
|
6
|
+
const READER = { userId: "00000000-0000-4000-8000-000000000001", canWrite: false, tokenId: "catalog-check" };
|
|
7
|
+
const WRITER = { ...READER, canWrite: true };
|
|
8
|
+
/** Every difference between the configured catalog and the server, each list in the catalog's order, then the server's. */
|
|
9
|
+
export async function getToolCatalogDifferences(config, createServer) {
|
|
10
|
+
const catalog = getMcpAccessOptions(config).tools;
|
|
11
|
+
const forReaders = await listServerTools(createServer, READER);
|
|
12
|
+
const forWriters = await listServerTools(createServer, WRITER);
|
|
13
|
+
const listed = new Set(catalog.map((tool) => tool.name));
|
|
14
|
+
return {
|
|
15
|
+
missing: catalog.filter((tool) => !(tool.access === "read" ? forReaders.includes(tool.name) && forWriters.includes(tool.name) : forWriters.includes(tool.name))).map((tool) => tool.name),
|
|
16
|
+
unlisted: [...new Set([...forReaders, ...forWriters])].filter((name) => !listed.has(name)),
|
|
17
|
+
writeForReaders: catalog.filter((tool) => tool.access === "write" && forReaders.includes(tool.name)).map((tool) => tool.name),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Throws a list of every difference, e.g. in a unit test next to the server. */
|
|
21
|
+
export async function expectToolCatalogMatchesServer(config, createServer) {
|
|
22
|
+
const differences = await getToolCatalogDifferences(config, createServer);
|
|
23
|
+
const problems = [
|
|
24
|
+
differences.missing.length > 0 ? `missing from the server: ${differences.missing.join(", ")}` : null,
|
|
25
|
+
differences.unlisted.length > 0 ? `registered but not in the catalog: ${differences.unlisted.join(", ")}` : null,
|
|
26
|
+
differences.writeForReaders.length > 0 ? `write tools a read-only token gets: ${differences.writeForReaders.join(", ")}` : null,
|
|
27
|
+
].filter((problem) => problem !== null);
|
|
28
|
+
if (problems.length > 0) {
|
|
29
|
+
throw new Error(`@softure-ai/mcp-access: the tool catalog in softure.config.ts does not match the MCP server: ${problems.join("; ")}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** The tool names a fresh server lists for the identity, asked the way the endpoint serves it. */
|
|
33
|
+
async function listServerTools(createServer, identity) {
|
|
34
|
+
const handler = createMcpHandler(() => createServer(identity), { responseMode: "json" });
|
|
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 !== "");
|
|
45
|
+
}
|
|
46
|
+
/** The JSON-RPC answer, sent as JSON or as one SSE `message` event. */
|
|
47
|
+
function readAnswer(body) {
|
|
48
|
+
const data = body.startsWith("event:") ? (body.split("\n").find((line) => line.startsWith("data: "))?.slice("data: ".length) ?? "null") : body;
|
|
49
|
+
return (JSON.parse(data) ?? {});
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../src/testing/catalog.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,8BAA8B,EAAE,yBAAyB,EAAE,KAAK,sBAAsB,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/testing/index.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,OAAO,EAAE,8BAA8B,EAAE,yBAAyB,EAA+B,MAAM,cAAc,CAAC"}
|
package/module.json
CHANGED
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softure-ai/mcp-access",
|
|
3
|
-
"version": "0.1.
|
|
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.",
|
|
3
|
+
"version": "0.1.13",
|
|
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",
|
|
7
|
-
"sideEffects":
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"./dist/stdio/register.js",
|
|
9
|
+
"./src/stdio/register.ts"
|
|
10
|
+
],
|
|
8
11
|
"engines": {
|
|
9
12
|
"node": ">=22"
|
|
10
13
|
},
|
|
@@ -27,6 +30,9 @@
|
|
|
27
30
|
"access": "public",
|
|
28
31
|
"provenance": true
|
|
29
32
|
},
|
|
33
|
+
"bin": {
|
|
34
|
+
"softure-mcp": "./dist/cli/bin.js"
|
|
35
|
+
},
|
|
30
36
|
"exports": {
|
|
31
37
|
".": {
|
|
32
38
|
"@softure-ai/source": "./src/index.ts",
|
|
@@ -47,6 +53,26 @@
|
|
|
47
53
|
"@softure-ai/source": "./src/ui/index.ts",
|
|
48
54
|
"types": "./dist/ui/index.d.ts",
|
|
49
55
|
"default": "./dist/ui/index.js"
|
|
56
|
+
},
|
|
57
|
+
"./stdio": {
|
|
58
|
+
"@softure-ai/source": "./src/stdio/index.ts",
|
|
59
|
+
"types": "./dist/stdio/index.d.ts",
|
|
60
|
+
"default": "./dist/stdio/index.js"
|
|
61
|
+
},
|
|
62
|
+
"./stdio/register": {
|
|
63
|
+
"@softure-ai/source": "./src/stdio/register.ts",
|
|
64
|
+
"types": "./dist/stdio/register.d.ts",
|
|
65
|
+
"default": "./dist/stdio/register.js"
|
|
66
|
+
},
|
|
67
|
+
"./testing": {
|
|
68
|
+
"@softure-ai/source": "./src/testing/index.ts",
|
|
69
|
+
"types": "./dist/testing/index.d.ts",
|
|
70
|
+
"default": "./dist/testing/index.js"
|
|
71
|
+
},
|
|
72
|
+
"./cli": {
|
|
73
|
+
"@softure-ai/source": "./src/cli/index.ts",
|
|
74
|
+
"types": "./dist/cli/index.d.ts",
|
|
75
|
+
"default": "./dist/cli/index.js"
|
|
50
76
|
}
|
|
51
77
|
},
|
|
52
78
|
"scripts": {
|
|
@@ -54,9 +80,9 @@
|
|
|
54
80
|
},
|
|
55
81
|
"dependencies": {
|
|
56
82
|
"@softure-ai/auth": "^0.1.0",
|
|
57
|
-
"@softure-ai/core": "^0.1.
|
|
83
|
+
"@softure-ai/core": "^0.1.9",
|
|
58
84
|
"@softure-ai/db": "^0.1.0",
|
|
59
|
-
"@softure-ai/security": "^0.1.
|
|
85
|
+
"@softure-ai/security": "^0.1.8",
|
|
60
86
|
"zod": "^4.6.5"
|
|
61
87
|
},
|
|
62
88
|
"peerDependencies": {
|
package/src/cli/bin.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `softure-mcp` executable. All logic lives in command.ts and run.ts, which the tests call directly.
|
|
3
|
+
import { runMcpAccessCommand } from "./command.js";
|
|
4
|
+
|
|
5
|
+
process.exitCode = await runMcpAccessCommand({ argv: process.argv.slice(2), cwd: process.cwd() });
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The `softure-mcp` bin. It loads the app's config through `@softure-ai/core/cli`, like `softure-mail`
|
|
2
|
+
// (@softure-ai/mailing): the file must be loadable by Node itself (`.js`/`.mjs`, or `.ts` where Node
|
|
3
|
+
// strips types). Anything else uses an app script with `runMcpAccessCli` instead (see run.ts).
|
|
4
|
+
import { loadAppConfig, takeConfigOption } from "@softure-ai/core/cli";
|
|
5
|
+
import { EXIT_FAILED, EXIT_USAGE, MCP_ACCESS_USAGE, runMcpAccessCli, type CliOutput } from "./run.js";
|
|
6
|
+
|
|
7
|
+
export interface RunMcpAccessCommandOptions {
|
|
8
|
+
readonly argv: readonly string[];
|
|
9
|
+
readonly cwd: string;
|
|
10
|
+
readonly output?: CliOutput;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const APP_SCRIPT = { runner: "runMcpAccessCli", packageName: "@softure-ai/mcp-access" };
|
|
14
|
+
|
|
15
|
+
const consoleOutput: CliOutput = {
|
|
16
|
+
log: (line) => console.log(line),
|
|
17
|
+
error: (line) => console.error(line),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Loads the config (unless only help is asked for) and runs `runMcpAccessCli`. */
|
|
21
|
+
export async function runMcpAccessCommand(options: RunMcpAccessCommandOptions): Promise<number> {
|
|
22
|
+
const output = options.output ?? consoleOutput;
|
|
23
|
+
const taken = takeConfigOption(options.argv);
|
|
24
|
+
if (!taken.ok) {
|
|
25
|
+
output.error(`softure-mcp: ${taken.problem}`);
|
|
26
|
+
output.error(MCP_ACCESS_USAGE);
|
|
27
|
+
return EXIT_USAGE;
|
|
28
|
+
}
|
|
29
|
+
const { configPath, argv } = taken;
|
|
30
|
+
if (argv[0] !== "prune") return runMcpAccessCli({ argv, output });
|
|
31
|
+
|
|
32
|
+
const loaded = await loadAppConfig({ cwd: options.cwd, configPath, appScript: APP_SCRIPT });
|
|
33
|
+
if (!loaded.ok) {
|
|
34
|
+
output.error(`softure-mcp: ${loaded.problem}`);
|
|
35
|
+
return EXIT_FAILED;
|
|
36
|
+
}
|
|
37
|
+
return runMcpAccessCli({ config: loaded.config, argv, output });
|
|
38
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// `@softure-ai/mcp-access/cli`: the `softure-mcp` command for app scripts and container bundles.
|
|
2
|
+
export { runMcpAccessCommand, type RunMcpAccessCommandOptions } from "./command.js";
|
|
3
|
+
export { EXIT_FAILED, EXIT_OK, EXIT_USAGE, MCP_ACCESS_USAGE, runMcpAccessCli, type CliOutput, type RunMcpAccessCliOptions } from "./run.js";
|
package/src/cli/run.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// `softure-mcp`: the module's maintenance command. `prune` deletes expired access tokens and dead OAuth
|
|
2
|
+
// records (`pruneMcpAccess`), for a daily job such as a deploy `maintain` hook. The app passes its
|
|
3
|
+
// validated config, so the same function serves the bin and an app script a bundler packs:
|
|
4
|
+
//
|
|
5
|
+
// // scripts/mcp.ts
|
|
6
|
+
// import { runMcpAccessCli } from "@softure-ai/mcp-access/cli";
|
|
7
|
+
// import config from "../softure.config";
|
|
8
|
+
// process.exitCode = await runMcpAccessCli({ config, argv: process.argv.slice(2) });
|
|
9
|
+
import { errorLogLabel, systemClock, type Clock, type SoftureConfig, type SoftureDatabaseConfig } from "@softure-ai/core";
|
|
10
|
+
import { openCommandDatabase, type CommandDatabase } from "@softure-ai/db";
|
|
11
|
+
import { pruneMcpAccess } from "../server/prune.js";
|
|
12
|
+
|
|
13
|
+
export interface CliOutput {
|
|
14
|
+
readonly log: (line: string) => void;
|
|
15
|
+
readonly error: (line: string) => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RunMcpAccessCliOptions {
|
|
19
|
+
/** The app's config; only help needs none. */
|
|
20
|
+
readonly config?: SoftureConfig;
|
|
21
|
+
/** The arguments after the executable, e.g. `["prune"]`. */
|
|
22
|
+
readonly argv: readonly string[];
|
|
23
|
+
readonly output?: CliOutput;
|
|
24
|
+
/** Default: the wall clock. */
|
|
25
|
+
readonly clock?: Clock;
|
|
26
|
+
/** Default: the config's `database.handle`, else a connection on `database.url`. */
|
|
27
|
+
readonly openDatabase?: (database: SoftureDatabaseConfig) => Promise<CommandDatabase>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const EXIT_OK = 0;
|
|
31
|
+
export const EXIT_FAILED = 1;
|
|
32
|
+
export const EXIT_USAGE = 2;
|
|
33
|
+
|
|
34
|
+
export const MCP_ACCESS_USAGE = `Usage:
|
|
35
|
+
softure-mcp prune deletes expired access tokens, authorization codes, grants and abandoned clients`;
|
|
36
|
+
|
|
37
|
+
const consoleOutput: CliOutput = {
|
|
38
|
+
log: (line) => console.log(line),
|
|
39
|
+
error: (line) => console.error(line),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Runs one command and answers its exit code. */
|
|
43
|
+
export async function runMcpAccessCli(options: RunMcpAccessCliOptions): Promise<number> {
|
|
44
|
+
const output = options.output ?? consoleOutput;
|
|
45
|
+
const [command, ...rest] = options.argv;
|
|
46
|
+
if (command === undefined || command === "--help" || command === "help") {
|
|
47
|
+
output.log(MCP_ACCESS_USAGE);
|
|
48
|
+
return EXIT_OK;
|
|
49
|
+
}
|
|
50
|
+
if (command !== "prune" || rest.length > 0) {
|
|
51
|
+
output.error(command === "prune" ? `softure-mcp prune: unexpected argument "${rest[0] ?? ""}"` : `softure-mcp: unknown command "${command}"`);
|
|
52
|
+
output.error(MCP_ACCESS_USAGE);
|
|
53
|
+
return EXIT_USAGE;
|
|
54
|
+
}
|
|
55
|
+
return prune(options, output);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function prune(options: RunMcpAccessCliOptions, output: CliOutput): Promise<number> {
|
|
59
|
+
const database = options.config?.database ?? null;
|
|
60
|
+
if (options.config === undefined || database === null) {
|
|
61
|
+
output.error("softure-mcp prune: the config has no database; set database.url in softure.config");
|
|
62
|
+
return EXIT_FAILED;
|
|
63
|
+
}
|
|
64
|
+
let opened: CommandDatabase | undefined;
|
|
65
|
+
try {
|
|
66
|
+
opened = await (options.openDatabase ?? ((settings) => openCommandDatabase(settings, { max: 1 })))(database);
|
|
67
|
+
const removed = await pruneMcpAccess({ db: opened.handle.db, clock: options.clock ?? systemClock, config: options.config });
|
|
68
|
+
output.log(
|
|
69
|
+
`softure-mcp prune: removed ${String(removed.accessTokens)} access tokens, ${String(removed.codes)} authorization codes, ${String(removed.grants)} grants, ${String(removed.clients)} clients`,
|
|
70
|
+
);
|
|
71
|
+
return EXIT_OK;
|
|
72
|
+
} catch (error) {
|
|
73
|
+
// The kind only: a driver message can carry the database address or credentials.
|
|
74
|
+
output.error(`softure-mcp prune: failed: ${errorLogLabel(error)}`);
|
|
75
|
+
return EXIT_FAILED;
|
|
76
|
+
} finally {
|
|
77
|
+
await opened?.close();
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/contract.ts
CHANGED
|
@@ -9,8 +9,11 @@ export type McpAccessErrorCode =
|
|
|
9
9
|
| "mcp-access.token_not_found"
|
|
10
10
|
| "mcp-access.grant_not_found";
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
export type
|
|
12
|
+
/** What an app's `beforeIssue` gate may answer instead of letting a token be issued. */
|
|
13
|
+
export type McpIssueRefusalCode = "mcp-access.issue_refused" | "mcp-access.write_refused";
|
|
14
|
+
|
|
15
|
+
/** Every code the token page can show: its own, a gate's refusal, a missing session and the generic ones. */
|
|
16
|
+
export type TokenFormErrorCode = McpAccessErrorCode | McpIssueRefusalCode | "auth.unauthenticated" | CoreErrorCode;
|
|
14
17
|
|
|
15
18
|
/** What the app's MCP server factory receives for one request. */
|
|
16
19
|
export interface McpServerIdentity {
|
package/src/index.ts
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
// work and the endpoint are in `@softure-ai/mcp-access/server`, the Next.js adapter (route, page,
|
|
4
4
|
// actions) in `/next`, the token manager in `/ui`.
|
|
5
5
|
import { defineModule, resolveMigrationsDir } from "@softure-ai/core";
|
|
6
|
+
import type { RateLimitBucketInput } from "@softure-ai/security";
|
|
6
7
|
import { mcpAccessMessages } from "./messages/index.js";
|
|
7
8
|
import { mcpAccessOptionsSchema } from "./options.js";
|
|
9
|
+
import { DEFAULT_MCP_ACCESS_ROUTES } from "./routes.js";
|
|
8
10
|
import { checkAccessTokensTable } from "./server/health.js";
|
|
9
11
|
import { mcpAccessPrivacyContributor } from "./server/privacy.js";
|
|
10
12
|
|
|
@@ -16,14 +18,14 @@ export const MODULE_ID = "mcp-access";
|
|
|
16
18
|
* the traffic behind one address (several assistants share it), not for one token.
|
|
17
19
|
*/
|
|
18
20
|
export const MCP_RATE_LIMIT_BUCKETS = {
|
|
19
|
-
mcp: { limit: 200, windowMinutes: 15 },
|
|
21
|
+
mcp: { limit: 200, windowMinutes: 15, key: "ip" },
|
|
20
22
|
/**
|
|
21
23
|
* OAuth registration (per address) and token requests (per address and client id), when
|
|
22
24
|
* `oauth.enabled`. Enough for an assistant platform refreshing many users' tokens per client,
|
|
23
25
|
* too little for a script filling the clients table.
|
|
24
26
|
*/
|
|
25
|
-
"mcp-oauth": { limit: 60, windowMinutes: 15 },
|
|
26
|
-
} as const
|
|
27
|
+
"mcp-oauth": { limit: 60, windowMinutes: 15, key: "ip" },
|
|
28
|
+
} as const satisfies Readonly<Record<string, RateLimitBucketInput>>;
|
|
27
29
|
|
|
28
30
|
/**
|
|
29
31
|
* Enables MCP access in `softure.config.ts` (after `security` and `auth`):
|
|
@@ -32,20 +34,13 @@ export const MCP_RATE_LIMIT_BUCKETS = {
|
|
|
32
34
|
export const mcpAccess = defineModule({
|
|
33
35
|
manifest: {
|
|
34
36
|
id: MODULE_ID,
|
|
35
|
-
version: "0.1.
|
|
37
|
+
version: "0.1.13",
|
|
36
38
|
dependsOn: { security: "^0.1.0", auth: "^0.1.0" },
|
|
37
39
|
dbSchema: "mcp",
|
|
38
40
|
tables: ["access_tokens", "oauth_clients", "oauth_authorization_codes", "oauth_grants"],
|
|
39
41
|
env: [],
|
|
40
42
|
switches: [],
|
|
41
|
-
routes: {
|
|
42
|
-
page: "/account/mcp",
|
|
43
|
-
endpoint: "/api/mcp",
|
|
44
|
-
oauthConsent: "/oauth/authorize",
|
|
45
|
-
oauthDecision: "/api/oauth/authorize",
|
|
46
|
-
oauthToken: "/api/oauth/token",
|
|
47
|
-
oauthRegister: "/api/oauth/register",
|
|
48
|
-
},
|
|
43
|
+
routes: { ...DEFAULT_MCP_ACCESS_ROUTES },
|
|
49
44
|
mount: [
|
|
50
45
|
{ kind: "page", path: "app/account/mcp/page.tsx", export: "McpAccessPage" },
|
|
51
46
|
{ kind: "route-handler", path: "app/api/mcp/route.ts", export: "createMcpRoute" },
|
|
@@ -81,6 +76,7 @@ export type {
|
|
|
81
76
|
IssuedToken,
|
|
82
77
|
IssueTokenFormState,
|
|
83
78
|
McpAccessErrorCode,
|
|
79
|
+
McpIssueRefusalCode,
|
|
84
80
|
McpClientSetup,
|
|
85
81
|
McpServerIdentity,
|
|
86
82
|
OAuthGrantView,
|
|
@@ -105,6 +101,7 @@ export {
|
|
|
105
101
|
type McpToolDefinition,
|
|
106
102
|
type McpToolDefinitionInput,
|
|
107
103
|
} from "./options.js";
|
|
104
|
+
export { DEFAULT_MCP_ACCESS_ROUTES } from "./routes.js";
|
|
108
105
|
export { readRequestOrigin, type McpOriginRequest, type McpOrigins } from "./origins.js";
|
|
109
106
|
export { accessTokens, oauthAuthorizationCodes, oauthClients, oauthGrants, type OAuthClientRow, type OAuthTokenEndpointAuthMethod } from "./schema.js";
|
|
110
107
|
export { getAccessTokenStatus, type AccessTokenStatusOptions } from "./token-status.js";
|
package/src/messages/en.ts
CHANGED
|
@@ -92,6 +92,8 @@ export const en = {
|
|
|
92
92
|
token_limit_reached: "You already hold the most active tokens allowed. Revoke one before creating another.",
|
|
93
93
|
token_not_found: "This token no longer exists. It may have been revoked already.",
|
|
94
94
|
grant_not_found: "This app is no longer connected.",
|
|
95
|
+
issue_refused: "Your account cannot create tokens right now.",
|
|
96
|
+
write_refused: "Your account cannot create tokens with write access. Create a read-only token instead.",
|
|
95
97
|
},
|
|
96
98
|
auth: {
|
|
97
99
|
unauthenticated: "Your session has ended. Log in again.",
|
package/src/messages/pl.ts
CHANGED
|
@@ -94,6 +94,8 @@ export const pl: typeof en = {
|
|
|
94
94
|
token_limit_reached: "Masz już najwięcej aktywnych tokenów, ile można. Unieważnij któryś, zanim utworzysz nowy.",
|
|
95
95
|
token_not_found: "Ten token już nie istnieje. Mógł zostać unieważniony.",
|
|
96
96
|
grant_not_found: "Ta aplikacja nie jest już połączona.",
|
|
97
|
+
issue_refused: "Twoje konto nie może teraz tworzyć tokenów.",
|
|
98
|
+
write_refused: "Twoje konto nie może tworzyć tokenów z prawem zapisu. Utwórz token tylko do odczytu.",
|
|
97
99
|
},
|
|
98
100
|
auth: {
|
|
99
101
|
unauthenticated: "Twoja sesja wygasła. Zaloguj się ponownie.",
|
package/src/next/actions.ts
CHANGED
|
@@ -4,18 +4,17 @@
|
|
|
4
4
|
// which the client controls, docs/02 §8) before the form is read, and every query is scoped to
|
|
5
5
|
// that owner. Unexpected failures become `safeError` codes; the token never reaches a log.
|
|
6
6
|
import { getCurrentUser } from "@softure-ai/auth/next";
|
|
7
|
-
import { errorLogLabel,
|
|
7
|
+
import { errorLogLabel, safeError, type SoftureConfig } from "@softure-ai/core";
|
|
8
8
|
import { getSoftureConfig } from "@softure-ai/core/next";
|
|
9
9
|
import { revalidatePath } from "next/cache";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
import { getMcpClientSetup } from "../client-setup.js";
|
|
12
11
|
import type { IssueTokenFormState, RevokeGrantFormState, RevokeTokenFormState } from "../contract.js";
|
|
13
12
|
import { MAX_TOKEN_NAME_LENGTH } from "../options.js";
|
|
14
|
-
import {
|
|
13
|
+
import { getMcpAccessRoutes } from "../server/options.js";
|
|
15
14
|
import { revokeOAuthGrant } from "../server/oauth.js";
|
|
16
|
-
import {
|
|
17
|
-
import { getMcpAccessContext
|
|
18
|
-
import {
|
|
15
|
+
import { revokeAccessToken } from "../server/tokens.js";
|
|
16
|
+
import { getMcpAccessContext } from "./context.js";
|
|
17
|
+
import { issueToken } from "./issue-token.js";
|
|
19
18
|
|
|
20
19
|
/** Longer values are cut: the server functions refuse them anyway, and nothing huge is echoed back. */
|
|
21
20
|
const MAX_FIELD_LENGTH = MAX_TOKEN_NAME_LENGTH * 4;
|
|
@@ -24,11 +23,6 @@ const text = z
|
|
|
24
23
|
.catch("")
|
|
25
24
|
.transform((value) => value.slice(0, MAX_FIELD_LENGTH));
|
|
26
25
|
|
|
27
|
-
const issueInput = z.object({
|
|
28
|
-
name: text,
|
|
29
|
-
// A checked HTML checkbox sends "on"; an unchecked one sends nothing, which means read only.
|
|
30
|
-
canWrite: z.string().nullable().catch(null),
|
|
31
|
-
});
|
|
32
26
|
const revokeInput = z.object({ id: text });
|
|
33
27
|
|
|
34
28
|
function reportFailure(operation: string, error: unknown) {
|
|
@@ -40,40 +34,9 @@ function refreshPage(config: SoftureConfig): void {
|
|
|
40
34
|
revalidatePath(getMcpAccessRoutes(config).page);
|
|
41
35
|
}
|
|
42
36
|
|
|
43
|
-
/** Issues a token for the signed-in user and returns its plaintext with the setup snippets, once. */
|
|
44
|
-
export async function issueTokenAction(
|
|
45
|
-
|
|
46
|
-
const user = await getCurrentUser();
|
|
47
|
-
if (user === null) return { status: "error", error: "auth.unauthenticated" };
|
|
48
|
-
|
|
49
|
-
// Every field has a `catch`, so parsing cannot fail.
|
|
50
|
-
const input = issueInput.parse({ name: formData.get("name"), canWrite: formData.get("canWrite") });
|
|
51
|
-
try {
|
|
52
|
-
// Resolved before issuing: a failure afterwards would leave a token nobody received.
|
|
53
|
-
const endpointUrl = getMcpEndpointUrl(config, await getRequestOrigins(config, getMcpAccessRoutes(config).page));
|
|
54
|
-
const result = await issueAccessToken(await getMcpAccessContext(config), { userId: user.id, name: input.name, canWrite: input.canWrite !== null });
|
|
55
|
-
if (!result.ok) return { status: "error", error: result.error };
|
|
56
|
-
refreshPage(config);
|
|
57
|
-
const issued = result.value;
|
|
58
|
-
const messages = getMcpAccessMessages(config);
|
|
59
|
-
return {
|
|
60
|
-
status: "ok",
|
|
61
|
-
issued: {
|
|
62
|
-
id: issued.id,
|
|
63
|
-
name: issued.name,
|
|
64
|
-
canWrite: issued.canWrite,
|
|
65
|
-
expiresText: formatMessage(messages.issued.expires, { date: formatDate(config, issued.expiresAt) }),
|
|
66
|
-
setup: getMcpClientSetup({
|
|
67
|
-
serverName: getMcpAccessOptions(config).serverName,
|
|
68
|
-
endpointUrl,
|
|
69
|
-
token: issued.token,
|
|
70
|
-
promptTemplate: messages.setup.assistantPrompt,
|
|
71
|
-
}),
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
} catch (error) {
|
|
75
|
-
return { status: "error", error: reportFailure("issuing a token", error) };
|
|
76
|
-
}
|
|
37
|
+
/** Issues a token for the signed-in user and returns its plaintext with the setup snippets, once. No gate: see `issueToken`. */
|
|
38
|
+
export async function issueTokenAction(previous: IssueTokenFormState, formData: FormData): Promise<IssueTokenFormState> {
|
|
39
|
+
return issueToken(previous, formData);
|
|
77
40
|
}
|
|
78
41
|
|
|
79
42
|
/** Revokes one of the signed-in user's tokens; another account's id matches nothing. */
|
package/src/next/format.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Dates on the token page, in the app's locale and time zone.
|
|
2
|
-
import type
|
|
2
|
+
import { formatCalendarDay, toCalendarDay, type SoftureConfig } from "@softure-ai/core";
|
|
3
3
|
|
|
4
4
|
export function formatDate(config: SoftureConfig, date: Date): string {
|
|
5
|
-
return
|
|
5
|
+
return formatCalendarDay(toCalendarDay(date, config.timezone), config.locale, "long");
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export function formatDateTime(config: SoftureConfig, date: Date): string {
|
package/src/next/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export {
|
|
|
10
10
|
getProtectedResourceMetadataRoute,
|
|
11
11
|
registerOAuthClientRoute,
|
|
12
12
|
} from "./oauth-routes.js";
|
|
13
|
-
export { getMcpAccessContext } from "./context.js";
|
|
14
|
-
export {
|
|
13
|
+
export { getMcpAccessContext, getRequestOrigins } from "./context.js";
|
|
14
|
+
export { issueToken, type BeforeIssueGate, type IssueGateInput, type IssueTokenOptions } from "./issue-token.js";
|
|
15
|
+
export { McpAccessPage, type McpAccessPageProps } from "./pages.js";
|
|
15
16
|
export { createMcpRoute } from "./route.js";
|