@softure-ai/mcp-access 0.0.0-stage → 0.1.6

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.
Files changed (122) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +177 -2
  4. package/dist/client-setup.d.ts +32 -0
  5. package/dist/client-setup.d.ts.map +1 -0
  6. package/dist/client-setup.js +57 -0
  7. package/dist/client-setup.js.map +1 -0
  8. package/dist/contract.d.ts +80 -0
  9. package/dist/contract.d.ts.map +1 -0
  10. package/dist/contract.js +2 -0
  11. package/dist/contract.js.map +1 -0
  12. package/dist/index.d.ts +119 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +50 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/messages/en.d.ts +77 -0
  17. package/dist/messages/en.d.ts.map +1 -0
  18. package/dist/messages/en.js +77 -0
  19. package/dist/messages/en.js.map +1 -0
  20. package/dist/messages/index.d.ts +161 -0
  21. package/dist/messages/index.d.ts.map +1 -0
  22. package/dist/messages/index.js +14 -0
  23. package/dist/messages/index.js.map +1 -0
  24. package/dist/messages/pl.d.ts +3 -0
  25. package/dist/messages/pl.d.ts.map +1 -0
  26. package/dist/messages/pl.js +77 -0
  27. package/dist/messages/pl.js.map +1 -0
  28. package/dist/next/actions.d.ts +6 -0
  29. package/dist/next/actions.d.ts.map +1 -0
  30. package/dist/next/actions.js +88 -0
  31. package/dist/next/actions.js.map +1 -0
  32. package/dist/next/context.d.ts +4 -0
  33. package/dist/next/context.d.ts.map +1 -0
  34. package/dist/next/context.js +13 -0
  35. package/dist/next/context.js.map +1 -0
  36. package/dist/next/format.d.ts +4 -0
  37. package/dist/next/format.d.ts.map +1 -0
  38. package/dist/next/format.js +7 -0
  39. package/dist/next/format.js.map +1 -0
  40. package/dist/next/index.d.ts +4 -0
  41. package/dist/next/index.d.ts.map +1 -0
  42. package/dist/next/index.js +6 -0
  43. package/dist/next/index.js.map +1 -0
  44. package/dist/next/pages.d.ts +2 -0
  45. package/dist/next/pages.d.ts.map +1 -0
  46. package/dist/next/pages.js +61 -0
  47. package/dist/next/pages.js.map +1 -0
  48. package/dist/next/route.d.ts +3 -0
  49. package/dist/next/route.d.ts.map +1 -0
  50. package/dist/next/route.js +24 -0
  51. package/dist/next/route.js.map +1 -0
  52. package/dist/options.d.ts +43 -0
  53. package/dist/options.d.ts.map +1 -0
  54. package/dist/options.js +47 -0
  55. package/dist/options.js.map +1 -0
  56. package/dist/schema.d.ts +145 -0
  57. package/dist/schema.d.ts.map +1 -0
  58. package/dist/schema.js +18 -0
  59. package/dist/schema.js.map +1 -0
  60. package/dist/server/endpoint.d.ts +26 -0
  61. package/dist/server/endpoint.d.ts.map +1 -0
  62. package/dist/server/endpoint.js +107 -0
  63. package/dist/server/endpoint.js.map +1 -0
  64. package/dist/server/health.d.ts +3 -0
  65. package/dist/server/health.d.ts.map +1 -0
  66. package/dist/server/health.js +11 -0
  67. package/dist/server/health.js.map +1 -0
  68. package/dist/server/index.d.ts +6 -0
  69. package/dist/server/index.d.ts.map +1 -0
  70. package/dist/server/index.js +8 -0
  71. package/dist/server/index.js.map +1 -0
  72. package/dist/server/options.d.ts +20 -0
  73. package/dist/server/options.d.ts.map +1 -0
  74. package/dist/server/options.js +40 -0
  75. package/dist/server/options.js.map +1 -0
  76. package/dist/server/privacy.d.ts +15 -0
  77. package/dist/server/privacy.d.ts.map +1 -0
  78. package/dist/server/privacy.js +36 -0
  79. package/dist/server/privacy.js.map +1 -0
  80. package/dist/server/tokens.d.ts +66 -0
  81. package/dist/server/tokens.d.ts.map +1 -0
  82. package/dist/server/tokens.js +138 -0
  83. package/dist/server/tokens.js.map +1 -0
  84. package/dist/token-status.d.ts +9 -0
  85. package/dist/token-status.d.ts.map +1 -0
  86. package/dist/token-status.js +14 -0
  87. package/dist/token-status.js.map +1 -0
  88. package/dist/ui/index.d.ts +2 -0
  89. package/dist/ui/index.d.ts.map +1 -0
  90. package/dist/ui/index.js +4 -0
  91. package/dist/ui/index.js.map +1 -0
  92. package/dist/ui/token-manager.d.ts +43 -0
  93. package/dist/ui/token-manager.d.ts.map +1 -0
  94. package/dist/ui/token-manager.js +50 -0
  95. package/dist/ui/token-manager.js.map +1 -0
  96. package/migrations/0001_create_access_tokens.sql +24 -0
  97. package/module.json +15 -0
  98. package/package.json +69 -4
  99. package/src/client-setup.ts +71 -0
  100. package/src/contract.ts +80 -0
  101. package/src/index.ts +81 -0
  102. package/src/messages/en.ts +76 -0
  103. package/src/messages/index.ts +18 -0
  104. package/src/messages/pl.ts +78 -0
  105. package/src/next/actions.ts +91 -0
  106. package/src/next/context.ts +14 -0
  107. package/src/next/format.ts +10 -0
  108. package/src/next/index.ts +5 -0
  109. package/src/next/next-modules.d.ts +16 -0
  110. package/src/next/pages.tsx +80 -0
  111. package/src/next/route.ts +24 -0
  112. package/src/options.ts +58 -0
  113. package/src/schema.ts +19 -0
  114. package/src/server/endpoint.ts +141 -0
  115. package/src/server/health.ts +12 -0
  116. package/src/server/index.ts +36 -0
  117. package/src/server/options.ts +54 -0
  118. package/src/server/privacy.ts +49 -0
  119. package/src/server/tokens.ts +188 -0
  120. package/src/token-status.ts +27 -0
  121. package/src/ui/index.ts +11 -0
  122. package/src/ui/token-manager.tsx +254 -0
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@softure-ai/mcp-access",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.6",
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.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "engines": {
9
+ "node": ">=22"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/SOFTURE/AI.git",
14
+ "directory": "modules/mcp-access"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "CHANGELOG.md",
19
+ "src",
20
+ "!src/**/*.test.ts",
21
+ "!src/**/*.test.tsx",
22
+ "migrations",
23
+ "module.json"
24
+ ],
25
+ "publishConfig": {
26
+ "access": "public",
27
+ "provenance": true
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "@softure-ai/source": "./src/index.ts",
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ },
35
+ "./server": {
36
+ "@softure-ai/source": "./src/server/index.ts",
37
+ "types": "./dist/server/index.d.ts",
38
+ "default": "./dist/server/index.js"
39
+ },
40
+ "./next": {
41
+ "@softure-ai/source": "./src/next/index.ts",
42
+ "types": "./dist/next/index.d.ts",
43
+ "default": "./dist/next/index.js"
44
+ },
45
+ "./ui": {
46
+ "@softure-ai/source": "./src/ui/index.ts",
47
+ "types": "./dist/ui/index.d.ts",
48
+ "default": "./dist/ui/index.js"
49
+ }
50
+ },
51
+ "scripts": {
52
+ "build": "tsc -p tsconfig.build.json"
53
+ },
54
+ "dependencies": {
55
+ "@softure-ai/auth": "^0.1.0",
56
+ "@softure-ai/core": "^0.1.0",
57
+ "@softure-ai/db": "^0.1.0",
58
+ "@softure-ai/security": "^0.1.0",
59
+ "zod": "^4.6.5"
60
+ },
61
+ "peerDependencies": {
62
+ "@modelcontextprotocol/server": "^2.0.0",
63
+ "@softure-ai/ui": "^0.1.0",
64
+ "drizzle-orm": "^0.45.2",
65
+ "next": "^16.0.0",
66
+ "react": "^19.0.0"
67
+ },
68
+ "devDependencies": {
69
+ "@softure-ai/ui": "^0.1.7"
70
+ }
71
+ }
@@ -0,0 +1,71 @@
1
+ // Ready-to-paste MCP client setup, built around the plaintext token (FIRE_TRACKER
2
+ // `src/lib/mcp-client-config.ts`, generalised to the app's server name and URL). Pure functions:
3
+ // the issue action calls them once, while the plaintext exists.
4
+ import { formatMessage } from "@softure-ai/core";
5
+ import type { McpClientSetup } from "./contract.js";
6
+
7
+ export interface McpClientSetupInput {
8
+ readonly serverName: string;
9
+ readonly endpointUrl: string;
10
+ readonly token: string;
11
+ /** The assistant prompt template from the messages: `{serverName}` and `{command}`. */
12
+ readonly promptTemplate: string;
13
+ }
14
+
15
+ /**
16
+ * The `Authorization` value, with its scheme. Clients with a header field send it verbatim, and
17
+ * the endpoint refuses a header without `Bearer `.
18
+ */
19
+ export function getAuthorizationHeader(token: string): string {
20
+ return `Bearer ${token.trim()}`;
21
+ }
22
+
23
+ /**
24
+ * `claude mcp add` with the token in it, not a placeholder: the plaintext is shown once, so this
25
+ * is the only moment a working command can be built for the user. `--scope user` connects the
26
+ * server everywhere, not only in the folder the terminal happens to be in.
27
+ */
28
+ export function getClaudeCodeCommand(serverName: string, endpointUrl: string, token: string): string {
29
+ return ["claude mcp add --transport http --scope user", serverName, endpointUrl, `--header "Authorization: ${getAuthorizationHeader(token)}"`].join(" ");
30
+ }
31
+
32
+ /** The `mcpServers` block for clients configured by a file. */
33
+ export function getJsonConfig(serverName: string, endpointUrl: string, token: string): string {
34
+ const server = { type: "http", url: endpointUrl, headers: { Authorization: getAuthorizationHeader(token) } };
35
+ return JSON.stringify({ mcpServers: { [serverName]: server } }, null, 2);
36
+ }
37
+
38
+ /**
39
+ * Claude Desktop's file runs local servers only, so a remote one goes through `mcp-remote`. The
40
+ * header sits in `args` without a space (`Authorization:${AUTH_HEADER}`) and its value in `env`:
41
+ * Claude Desktop on Windows splits arguments on spaces. `${AUTH_HEADER}` is a literal that
42
+ * `mcp-remote` substitutes. `-y` keeps `npx` from waiting for a confirmation nobody can give.
43
+ */
44
+ export function getDesktopConfig(serverName: string, endpointUrl: string, token: string): string {
45
+ const server = {
46
+ command: "npx",
47
+ args: ["-y", "mcp-remote", endpointUrl, "--header", "Authorization:${AUTH_HEADER}"],
48
+ env: { AUTH_HEADER: getAuthorizationHeader(token) },
49
+ };
50
+ return JSON.stringify({ mcpServers: { [serverName]: server } }, null, 2);
51
+ }
52
+
53
+ /** Opens Claude Code with the prompt; Claude Code shows it and waits for Enter, it runs nothing alone. */
54
+ export function getClaudeCodeLink(prompt: string): string {
55
+ return `claude-cli://open?q=${encodeURIComponent(prompt)}`;
56
+ }
57
+
58
+ export function getMcpClientSetup({ serverName, endpointUrl, token, promptTemplate }: McpClientSetupInput): McpClientSetup {
59
+ const claudeCodeCommand = getClaudeCodeCommand(serverName, endpointUrl, token);
60
+ const assistantPrompt = formatMessage(promptTemplate, { serverName, command: claudeCodeCommand });
61
+ return {
62
+ serverName,
63
+ endpointUrl,
64
+ claudeCodeCommand,
65
+ assistantPrompt,
66
+ claudeCodeLink: getClaudeCodeLink(assistantPrompt),
67
+ jsonConfig: getJsonConfig(serverName, endpointUrl, token),
68
+ desktopConfig: getDesktopConfig(serverName, endpointUrl, token),
69
+ authorizationHeader: getAuthorizationHeader(token),
70
+ };
71
+ }
@@ -0,0 +1,80 @@
1
+ // Result types and error codes of the mcp-access module. No user-facing copy here: the UI
2
+ // translates codes through `messages` (docs/02-module-standard.md §6).
3
+ import type { CoreErrorCode } from "@softure-ai/core";
4
+
5
+ export type McpAccessErrorCode =
6
+ | "mcp-access.name_required"
7
+ | "mcp-access.name_too_long"
8
+ | "mcp-access.token_limit_reached"
9
+ | "mcp-access.token_not_found";
10
+
11
+ /** Every code the token page can show: its own, a missing session and the generic ones. */
12
+ export type TokenFormErrorCode = McpAccessErrorCode | "auth.unauthenticated" | CoreErrorCode;
13
+
14
+ /** What the app's MCP server factory receives for one request. */
15
+ export interface McpServerIdentity {
16
+ /** The auth user id of the token's owner: every query of the server is scoped to it. */
17
+ readonly userId: string;
18
+ /** Whether write tools may be registered: the app allows writes and the token was issued for them. */
19
+ readonly canWrite: boolean;
20
+ /** The token's id, for the server's own logs. Never the token. */
21
+ readonly tokenId: string;
22
+ }
23
+
24
+ /** A stored token as its owner sees it. The hash never leaves the server functions. */
25
+ export interface AccessTokenView {
26
+ readonly id: string;
27
+ readonly name: string;
28
+ /** Issued with write access (it still writes only while the app allows writes). */
29
+ readonly canWrite: boolean;
30
+ readonly createdAt: Date;
31
+ readonly expiresAt: Date;
32
+ readonly lastUsedAt: Date | null;
33
+ }
34
+
35
+ /** Where a token stands on its expiry, for the list. */
36
+ export type AccessTokenStatus =
37
+ | { readonly kind: "active" }
38
+ /** `daysLeft`: calendar days in the app's time zone, from 0 (today) to the warning threshold. */
39
+ | { readonly kind: "expiring"; readonly daysLeft: number }
40
+ | { readonly kind: "expired" };
41
+
42
+ /** Ready-to-paste setup for MCP clients, built around the plaintext token. */
43
+ export interface McpClientSetup {
44
+ readonly serverName: string;
45
+ readonly endpointUrl: string;
46
+ /** `claude mcp add …` for Claude Code, with the token in it. */
47
+ readonly claudeCodeCommand: string;
48
+ /** A prompt asking the assistant to run that command and confirm the connection. */
49
+ readonly assistantPrompt: string;
50
+ /** A `claude-cli://` link that opens Claude Code with the prompt (it runs nothing by itself). */
51
+ readonly claudeCodeLink: string;
52
+ /** `mcpServers` JSON for clients configured by file (Cursor, VS Code, Windsurf). */
53
+ readonly jsonConfig: string;
54
+ /** Claude Desktop's file, bridged through `mcp-remote` (it cannot send headers itself). */
55
+ readonly desktopConfig: string;
56
+ /** The `Authorization` header value, for clients with a header field. */
57
+ readonly authorizationHeader: string;
58
+ }
59
+
60
+ /** A token right after it was issued: the only moment its plaintext exists outside the client. */
61
+ export interface IssuedToken {
62
+ readonly id: string;
63
+ readonly name: string;
64
+ readonly canWrite: boolean;
65
+ /** The expiry, already formatted in the app's locale and time zone. */
66
+ readonly expiresText: string;
67
+ readonly setup: McpClientSetup;
68
+ }
69
+
70
+ /** What the issue action returns to its form (`useActionState`). */
71
+ export type IssueTokenFormState =
72
+ | { readonly status: "idle" }
73
+ | { readonly status: "ok"; readonly issued: IssuedToken }
74
+ | { readonly status: "error"; readonly error: TokenFormErrorCode };
75
+
76
+ /** What the revoke action returns to one token's form. */
77
+ export type RevokeTokenFormState =
78
+ | { readonly status: "idle" }
79
+ | { readonly status: "ok" }
80
+ | { readonly status: "error"; readonly error: TokenFormErrorCode };
package/src/index.ts ADDED
@@ -0,0 +1,81 @@
1
+ // Public API of @softure-ai/mcp-access: the module factory for softure.config.ts, its rate limit
2
+ // bucket, types, messages, table and the pure helpers for token status and client setup. Database
3
+ // work and the endpoint are in `@softure-ai/mcp-access/server`, the Next.js adapter (route, page,
4
+ // actions) in `/next`, the token manager in `/ui`.
5
+ import { defineModule, resolveMigrationsDir } from "@softure-ai/core";
6
+ import { mcpAccessMessages } from "./messages/index.js";
7
+ import { mcpAccessOptionsSchema } from "./options.js";
8
+ import { checkAccessTokensTable } from "./server/health.js";
9
+ import { mcpAccessPrivacyContributor } from "./server/privacy.js";
10
+
11
+ export const MODULE_ID = "mcp-access";
12
+
13
+ /**
14
+ * The rate limit bucket of `POST /api/mcp`, to spread into `security({ buckets })`: requests per
15
+ * client address, counted before the token is verified. Every request counts, so the limit is for
16
+ * the traffic behind one address (several assistants share it), not for one token.
17
+ */
18
+ export const MCP_RATE_LIMIT_BUCKETS = {
19
+ mcp: { limit: 200, windowMinutes: 15 },
20
+ } as const;
21
+
22
+ /**
23
+ * Enables MCP access in `softure.config.ts` (after `security` and `auth`):
24
+ * `mcpAccess({ serverName: "acme", tools: [{ name: "list_orders", access: "read", description: { en: "Lists your orders." } }] })`.
25
+ */
26
+ export const mcpAccess = defineModule({
27
+ manifest: {
28
+ id: MODULE_ID,
29
+ version: "0.1.6",
30
+ dependsOn: { security: "^0.1.0", auth: "^0.1.0" },
31
+ dbSchema: "mcp",
32
+ tables: ["access_tokens"],
33
+ env: [],
34
+ switches: [],
35
+ routes: { page: "/account/mcp", endpoint: "/api/mcp" },
36
+ mount: [
37
+ { kind: "page", path: "app/account/mcp/page.tsx", export: "McpAccessPage" },
38
+ { kind: "route-handler", path: "app/api/mcp/route.ts", export: "createMcpRoute" },
39
+ ],
40
+ privacy: { exports: true, deletes: true },
41
+ },
42
+ messages: mcpAccessMessages,
43
+ options: mcpAccessOptionsSchema,
44
+ migrations: { dir: resolveMigrationsDir(import.meta.url, "../migrations/") },
45
+ privacy: mcpAccessPrivacyContributor,
46
+ health: checkAccessTokensTable,
47
+ });
48
+
49
+ export {
50
+ getAuthorizationHeader,
51
+ getClaudeCodeCommand,
52
+ getClaudeCodeLink,
53
+ getDesktopConfig,
54
+ getJsonConfig,
55
+ getMcpClientSetup,
56
+ type McpClientSetupInput,
57
+ } from "./client-setup.js";
58
+ export type {
59
+ AccessTokenStatus,
60
+ AccessTokenView,
61
+ IssuedToken,
62
+ IssueTokenFormState,
63
+ McpAccessErrorCode,
64
+ McpClientSetup,
65
+ McpServerIdentity,
66
+ RevokeTokenFormState,
67
+ TokenFormErrorCode,
68
+ } from "./contract.js";
69
+ export { getTokenErrorMessage, mcpAccessMessages, type McpAccessMessages } from "./messages/index.js";
70
+ export {
71
+ MAX_TOKEN_NAME_LENGTH,
72
+ SERVER_NAME_PATTERN,
73
+ TOOL_NAME_PATTERN,
74
+ type McpAccessOptions,
75
+ type McpAccessOptionsInput,
76
+ type McpToolAccess,
77
+ type McpToolDefinition,
78
+ type McpToolDefinitionInput,
79
+ } from "./options.js";
80
+ export { accessTokens } from "./schema.js";
81
+ export { getAccessTokenStatus, type AccessTokenStatusOptions } from "./token-status.js";
@@ -0,0 +1,76 @@
1
+ export const en = {
2
+ page: {
3
+ title: "AI assistant access",
4
+ lead: "Connect an AI assistant such as Claude to your account over MCP. Each assistant gets its own token, which you can revoke at any time.",
5
+ },
6
+ tools: {
7
+ title: "What an assistant can do",
8
+ empty: "The app lists no tools yet.",
9
+ read: "Reads",
10
+ write: "Changes data",
11
+ writeUnavailable: "Changes data (turned off in this app)",
12
+ },
13
+ issue: {
14
+ title: "New token",
15
+ name: "Name",
16
+ nameHint: "Where you will use it, e.g. Laptop or Claude Desktop.",
17
+ canWrite: "Allow changes",
18
+ canWriteHint: "Without it, the assistant can only read.",
19
+ submit: "Create token",
20
+ pending: "Creating…",
21
+ limit: "You can hold up to {max} active tokens.",
22
+ },
23
+ issued: {
24
+ title: "Token created: {name}",
25
+ once: "Copy it now. It is shown only once; after you leave this page, only its name remains.",
26
+ expires: "Works until {date}.",
27
+ readOnly: "This token can only read.",
28
+ readWrite: "This token can read and change data.",
29
+ promptTitle: "Easiest: let Claude Code set it up",
30
+ promptHint: "Paste this into a conversation with Claude Code. It runs the command and checks the connection.",
31
+ openClaudeCode: "Open in Claude Code",
32
+ commandTitle: "Terminal command (Claude Code)",
33
+ jsonTitle: "Configuration file (Cursor, VS Code, Windsurf)",
34
+ desktopTitle: "Claude Desktop (claude_desktop_config.json)",
35
+ headerTitle: "Other clients: server address and Authorization header",
36
+ copy: "Copy",
37
+ copied: "Copied",
38
+ done: "Done, hide the token",
39
+ },
40
+ setup: {
41
+ assistantPrompt: "Connect to my {serverName} account over MCP.\nRun: {command}\nThen check /mcp and tell me whether the server \"{serverName}\" is connected.",
42
+ },
43
+ list: {
44
+ title: "Your tokens",
45
+ empty: "You have no tokens yet.",
46
+ readOnly: "Read only",
47
+ readWrite: "Read and change",
48
+ active: "Active",
49
+ expired: "Expired",
50
+ expiresToday: "Expires today",
51
+ expiresTomorrow: "Expires tomorrow",
52
+ expiresInDays: "Expires in {days} days",
53
+ expiresOn: "Valid until {date}",
54
+ expiredOn: "Expired on {date}",
55
+ createdOn: "Created {date}",
56
+ lastUsed: "Last used {date}",
57
+ neverUsed: "Not used yet",
58
+ revoke: "Revoke",
59
+ revoking: "Revoking…",
60
+ },
61
+ errors: {
62
+ "mcp-access": {
63
+ name_required: "Give the token a name, so you can tell it from the others.",
64
+ name_too_long: "The name is too long.",
65
+ token_limit_reached: "You already hold the most active tokens allowed. Revoke one before creating another.",
66
+ token_not_found: "This token no longer exists. It may have been revoked already.",
67
+ },
68
+ auth: {
69
+ unauthenticated: "Your session has ended. Log in again.",
70
+ },
71
+ core: {
72
+ database_failed: "Something went wrong on our side. Try again in a moment.",
73
+ unexpected: "Something went wrong on our side. Try again in a moment.",
74
+ },
75
+ },
76
+ };
@@ -0,0 +1,18 @@
1
+ import type { TokenFormErrorCode } from "../contract.js";
2
+ import { en } from "./en.js";
3
+ import { pl } from "./pl.js";
4
+
5
+ /** Complete default dictionaries; apps pass partial overrides per locale. */
6
+ export const mcpAccessMessages = { en, pl };
7
+
8
+ export type McpAccessMessages = typeof en;
9
+
10
+ /** The copy for an error code; an unknown code gets the generic failure. */
11
+ export function getTokenErrorMessage(messages: McpAccessMessages, code: TokenFormErrorCode): string {
12
+ const separator = code.lastIndexOf(".");
13
+ const namespace = code.slice(0, separator);
14
+ const name = code.slice(separator + 1);
15
+ const groups = messages.errors as Readonly<Record<string, Readonly<Record<string, string>>>>;
16
+ const group = Object.hasOwn(groups, namespace) ? groups[namespace] : undefined;
17
+ return (group !== undefined && Object.hasOwn(group, name) ? group[name] : undefined) ?? messages.errors.core.unexpected;
18
+ }
@@ -0,0 +1,78 @@
1
+ import type { en } from "./en.js";
2
+
3
+ export const pl: typeof en = {
4
+ page: {
5
+ title: "Dostęp dla asystenta AI",
6
+ lead: "Połącz asystenta AI, na przykład Claude, ze swoim kontem przez MCP. Każdy asystent dostaje własny token, który możesz w każdej chwili unieważnić.",
7
+ },
8
+ tools: {
9
+ title: "Co może asystent",
10
+ empty: "Aplikacja nie udostępnia jeszcze żadnych narzędzi.",
11
+ read: "Odczyt",
12
+ write: "Zmienia dane",
13
+ writeUnavailable: "Zmienia dane (wyłączone w tej aplikacji)",
14
+ },
15
+ issue: {
16
+ title: "Nowy token",
17
+ name: "Nazwa",
18
+ nameHint: "Gdzie go użyjesz, na przykład Laptop albo Claude Desktop.",
19
+ canWrite: "Pozwól na zmiany",
20
+ canWriteHint: "Bez tego asystent może tylko czytać.",
21
+ submit: "Utwórz token",
22
+ pending: "Tworzenie…",
23
+ limit: "Możesz mieć najwyżej {max} aktywnych tokenów.",
24
+ },
25
+ issued: {
26
+ title: "Utworzono token: {name}",
27
+ once: "Skopiuj go teraz. Pokazujemy go tylko raz; gdy opuścisz tę stronę, zostanie tylko jego nazwa.",
28
+ expires: "Działa do {date}.",
29
+ readOnly: "Ten token może tylko czytać.",
30
+ readWrite: "Ten token może czytać i zmieniać dane.",
31
+ promptTitle: "Najprościej: niech Claude Code sam to skonfiguruje",
32
+ promptHint: "Wklej to do rozmowy z Claude Code. Wykona polecenie i sprawdzi połączenie.",
33
+ openClaudeCode: "Otwórz w Claude Code",
34
+ commandTitle: "Polecenie w terminalu (Claude Code)",
35
+ jsonTitle: "Plik konfiguracyjny (Cursor, VS Code, Windsurf)",
36
+ desktopTitle: "Claude Desktop (claude_desktop_config.json)",
37
+ headerTitle: "Inne klienty: adres serwera i nagłówek Authorization",
38
+ copy: "Kopiuj",
39
+ copied: "Skopiowano",
40
+ done: "Gotowe, ukryj token",
41
+ },
42
+ setup: {
43
+ assistantPrompt: "Połącz się z moim kontem {serverName} przez MCP.\nWykonaj: {command}\nPotem sprawdź /mcp i powiedz mi, czy serwer „{serverName}” jest połączony.",
44
+ },
45
+ list: {
46
+ title: "Twoje tokeny",
47
+ empty: "Nie masz jeszcze żadnych tokenów.",
48
+ readOnly: "Tylko odczyt",
49
+ readWrite: "Odczyt i zmiany",
50
+ active: "Aktywny",
51
+ expired: "Wygasł",
52
+ expiresToday: "Wygasa dziś",
53
+ expiresTomorrow: "Wygasa jutro",
54
+ expiresInDays: "Wygasa za {days} dni",
55
+ expiresOn: "Ważny do {date}",
56
+ expiredOn: "Wygasł {date}",
57
+ createdOn: "Utworzony {date}",
58
+ lastUsed: "Ostatnio użyty {date}",
59
+ neverUsed: "Jeszcze nieużywany",
60
+ revoke: "Unieważnij",
61
+ revoking: "Unieważnianie…",
62
+ },
63
+ errors: {
64
+ "mcp-access": {
65
+ name_required: "Nadaj tokenowi nazwę, żeby odróżnić go od pozostałych.",
66
+ name_too_long: "Nazwa jest za długa.",
67
+ token_limit_reached: "Masz już najwięcej aktywnych tokenów, ile można. Unieważnij któryś, zanim utworzysz nowy.",
68
+ token_not_found: "Ten token już nie istnieje. Mógł zostać unieważniony.",
69
+ },
70
+ auth: {
71
+ unauthenticated: "Twoja sesja wygasła. Zaloguj się ponownie.",
72
+ },
73
+ core: {
74
+ database_failed: "Coś poszło nie tak po naszej stronie. Spróbuj ponownie za chwilę.",
75
+ unexpected: "Coś poszło nie tak po naszej stronie. Spróbuj ponownie za chwilę.",
76
+ },
77
+ },
78
+ };
@@ -0,0 +1,91 @@
1
+ "use server";
2
+
3
+ // The token page's server actions. The owner comes from the session (never from a bound argument,
4
+ // which the client controls, docs/02 §8) before the form is read, and every query is scoped to
5
+ // that owner. Unexpected failures become `safeError` codes; the token never reaches a log.
6
+ import { getCurrentUser } from "@softure-ai/auth/next";
7
+ import { errorLogLabel, formatMessage, safeError, type SoftureConfig } from "@softure-ai/core";
8
+ import { getSoftureConfig } from "@softure-ai/core/next";
9
+ import { revalidatePath } from "next/cache";
10
+ import { z } from "zod";
11
+ import { getMcpClientSetup } from "../client-setup.js";
12
+ import type { IssueTokenFormState, RevokeTokenFormState } from "../contract.js";
13
+ import { MAX_TOKEN_NAME_LENGTH } from "../options.js";
14
+ import { getMcpAccessMessages, getMcpAccessOptions, getMcpAccessRoutes, getMcpEndpointUrl } from "../server/options.js";
15
+ import { issueAccessToken, revokeAccessToken } from "../server/tokens.js";
16
+ import { getMcpAccessContext } from "./context.js";
17
+ import { formatDate } from "./format.js";
18
+
19
+ /** Longer values are cut: the server functions refuse them anyway, and nothing huge is echoed back. */
20
+ const MAX_FIELD_LENGTH = MAX_TOKEN_NAME_LENGTH * 4;
21
+ const text = z
22
+ .string()
23
+ .catch("")
24
+ .transform((value) => value.slice(0, MAX_FIELD_LENGTH));
25
+
26
+ const issueInput = z.object({
27
+ name: text,
28
+ // A checked HTML checkbox sends "on"; an unchecked one sends nothing, which means read only.
29
+ canWrite: z.string().nullable().catch(null),
30
+ });
31
+ const revokeInput = z.object({ id: text });
32
+
33
+ function reportFailure(operation: string, error: unknown) {
34
+ console.error(`@softure-ai/mcp-access: ${operation} failed: ${errorLogLabel(error)}`);
35
+ return safeError(error).error;
36
+ }
37
+
38
+ function refreshPage(config: SoftureConfig): void {
39
+ revalidatePath(getMcpAccessRoutes(config).page);
40
+ }
41
+
42
+ /** Issues a token for the signed-in user and returns its plaintext with the setup snippets, once. */
43
+ export async function issueTokenAction(_previous: IssueTokenFormState, formData: FormData): Promise<IssueTokenFormState> {
44
+ const config = getSoftureConfig();
45
+ const user = await getCurrentUser();
46
+ if (user === null) return { status: "error", error: "auth.unauthenticated" };
47
+
48
+ // Every field has a `catch`, so parsing cannot fail.
49
+ const input = issueInput.parse({ name: formData.get("name"), canWrite: formData.get("canWrite") });
50
+ try {
51
+ const result = await issueAccessToken(await getMcpAccessContext(config), { userId: user.id, name: input.name, canWrite: input.canWrite !== null });
52
+ if (!result.ok) return { status: "error", error: result.error };
53
+ refreshPage(config);
54
+ const issued = result.value;
55
+ const messages = getMcpAccessMessages(config);
56
+ return {
57
+ status: "ok",
58
+ issued: {
59
+ id: issued.id,
60
+ name: issued.name,
61
+ canWrite: issued.canWrite,
62
+ expiresText: formatMessage(messages.issued.expires, { date: formatDate(config, issued.expiresAt) }),
63
+ setup: getMcpClientSetup({
64
+ serverName: getMcpAccessOptions(config).serverName,
65
+ endpointUrl: getMcpEndpointUrl(config),
66
+ token: issued.token,
67
+ promptTemplate: messages.setup.assistantPrompt,
68
+ }),
69
+ },
70
+ };
71
+ } catch (error) {
72
+ return { status: "error", error: reportFailure("issuing a token", error) };
73
+ }
74
+ }
75
+
76
+ /** Revokes one of the signed-in user's tokens; another account's id matches nothing. */
77
+ export async function revokeTokenAction(_previous: RevokeTokenFormState, formData: FormData): Promise<RevokeTokenFormState> {
78
+ const config = getSoftureConfig();
79
+ const user = await getCurrentUser();
80
+ if (user === null) return { status: "error", error: "auth.unauthenticated" };
81
+
82
+ const input = revokeInput.parse({ id: formData.get("id") });
83
+ try {
84
+ const result = await revokeAccessToken(await getMcpAccessContext(config), { userId: user.id, tokenId: input.id });
85
+ if (!result.ok) return { status: "error", error: result.error };
86
+ refreshPage(config);
87
+ return { status: "ok" };
88
+ } catch (error) {
89
+ return { status: "error", error: reportFailure("revoking a token", error) };
90
+ }
91
+ }
@@ -0,0 +1,14 @@
1
+ // What the adapter hands the server functions: the registered config, the process-wide database
2
+ // handle and the wall clock.
3
+ import { systemClock, type SoftureConfig } from "@softure-ai/core";
4
+ import { getSoftureConfig } from "@softure-ai/core/next";
5
+ import { getConfiguredDatabase } from "@softure-ai/db";
6
+ import type { McpAccessContext } from "../server/tokens.js";
7
+
8
+ export async function getMcpAccessContext(config: SoftureConfig = getSoftureConfig()): Promise<McpAccessContext> {
9
+ if (config.database === null) {
10
+ throw new Error("@softure-ai/mcp-access: softure.config.ts has no database; mcp-access needs one");
11
+ }
12
+ const { db } = await getConfiguredDatabase(config.database);
13
+ return { db, clock: systemClock, config };
14
+ }
@@ -0,0 +1,10 @@
1
+ // Dates on the token page, in the app's locale and time zone.
2
+ import type { SoftureConfig } from "@softure-ai/core";
3
+
4
+ export function formatDate(config: SoftureConfig, date: Date): string {
5
+ return new Intl.DateTimeFormat(config.locale, { dateStyle: "long", timeZone: config.timezone }).format(date);
6
+ }
7
+
8
+ export function formatDateTime(config: SoftureConfig, date: Date): string {
9
+ return new Intl.DateTimeFormat(config.locale, { dateStyle: "medium", timeStyle: "short", timeZone: config.timezone }).format(date);
10
+ }
@@ -0,0 +1,5 @@
1
+ // The Next.js adapter of @softure-ai/mcp-access: the MCP route factory, the token page and its
2
+ // server actions (docs/02-module-standard.md §8).
3
+ export { issueTokenAction, revokeTokenAction } from "./actions.js";
4
+ export { McpAccessPage } from "./pages.js";
5
+ export { createMcpRoute } from "./route.js";
@@ -0,0 +1,16 @@
1
+ // Next.js has no `exports` map, so NodeNext resolution (tsconfig.base.json) only finds
2
+ // `next/navigation.js` and `next/headers.js`. The code must still import the bare specifiers:
3
+ // Next's bundler aliases them per runtime, and the `.js` form bypasses the alias (measured:
4
+ // `next build` failed to collect a route handler importing `next/navigation.js`, MODULE_UNPARSABLE
5
+ // on the vendored app-router context). These declarations give the bare specifiers their types.
6
+ declare module "next/navigation" {
7
+ export * from "next/navigation.js";
8
+ }
9
+
10
+ declare module "next/headers" {
11
+ export * from "next/headers.js";
12
+ }
13
+
14
+ declare module "next/cache" {
15
+ export * from "next/cache.js";
16
+ }