@softure-ai/mcp-access 0.0.0-stage → 0.1.5
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/LICENSE +21 -0
- package/README.md +177 -2
- package/dist/client-setup.d.ts +32 -0
- package/dist/client-setup.d.ts.map +1 -0
- package/dist/client-setup.js +57 -0
- package/dist/client-setup.js.map +1 -0
- package/dist/contract.d.ts +80 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +119 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/messages/en.d.ts +77 -0
- package/dist/messages/en.d.ts.map +1 -0
- package/dist/messages/en.js +77 -0
- package/dist/messages/en.js.map +1 -0
- package/dist/messages/index.d.ts +161 -0
- package/dist/messages/index.d.ts.map +1 -0
- package/dist/messages/index.js +14 -0
- package/dist/messages/index.js.map +1 -0
- package/dist/messages/pl.d.ts +3 -0
- package/dist/messages/pl.d.ts.map +1 -0
- package/dist/messages/pl.js +77 -0
- package/dist/messages/pl.js.map +1 -0
- package/dist/next/actions.d.ts +6 -0
- package/dist/next/actions.d.ts.map +1 -0
- package/dist/next/actions.js +88 -0
- package/dist/next/actions.js.map +1 -0
- package/dist/next/context.d.ts +4 -0
- package/dist/next/context.d.ts.map +1 -0
- package/dist/next/context.js +13 -0
- package/dist/next/context.js.map +1 -0
- package/dist/next/format.d.ts +4 -0
- package/dist/next/format.d.ts.map +1 -0
- package/dist/next/format.js +7 -0
- package/dist/next/format.js.map +1 -0
- package/dist/next/index.d.ts +4 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +6 -0
- package/dist/next/index.js.map +1 -0
- package/dist/next/pages.d.ts +2 -0
- package/dist/next/pages.d.ts.map +1 -0
- package/dist/next/pages.js +61 -0
- package/dist/next/pages.js.map +1 -0
- package/dist/next/route.d.ts +3 -0
- package/dist/next/route.d.ts.map +1 -0
- package/dist/next/route.js +24 -0
- package/dist/next/route.js.map +1 -0
- package/dist/options.d.ts +43 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +47 -0
- package/dist/options.js.map +1 -0
- package/dist/schema.d.ts +145 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +18 -0
- package/dist/schema.js.map +1 -0
- package/dist/server/endpoint.d.ts +26 -0
- package/dist/server/endpoint.d.ts.map +1 -0
- package/dist/server/endpoint.js +107 -0
- package/dist/server/endpoint.js.map +1 -0
- package/dist/server/health.d.ts +3 -0
- package/dist/server/health.d.ts.map +1 -0
- package/dist/server/health.js +11 -0
- package/dist/server/health.js.map +1 -0
- package/dist/server/index.d.ts +6 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +8 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/options.d.ts +20 -0
- package/dist/server/options.d.ts.map +1 -0
- package/dist/server/options.js +40 -0
- package/dist/server/options.js.map +1 -0
- package/dist/server/privacy.d.ts +15 -0
- package/dist/server/privacy.d.ts.map +1 -0
- package/dist/server/privacy.js +36 -0
- package/dist/server/privacy.js.map +1 -0
- package/dist/server/tokens.d.ts +66 -0
- package/dist/server/tokens.d.ts.map +1 -0
- package/dist/server/tokens.js +138 -0
- package/dist/server/tokens.js.map +1 -0
- package/dist/token-status.d.ts +9 -0
- package/dist/token-status.d.ts.map +1 -0
- package/dist/token-status.js +14 -0
- package/dist/token-status.js.map +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +4 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/token-manager.d.ts +43 -0
- package/dist/ui/token-manager.d.ts.map +1 -0
- package/dist/ui/token-manager.js +50 -0
- package/dist/ui/token-manager.js.map +1 -0
- package/migrations/0001_create_access_tokens.sql +24 -0
- package/module.json +15 -0
- package/package.json +65 -4
- package/src/client-setup.ts +71 -0
- package/src/contract.ts +80 -0
- package/src/index.ts +81 -0
- package/src/messages/en.ts +76 -0
- package/src/messages/index.ts +18 -0
- package/src/messages/pl.ts +78 -0
- package/src/next/actions.ts +91 -0
- package/src/next/context.ts +14 -0
- package/src/next/format.ts +10 -0
- package/src/next/index.ts +5 -0
- package/src/next/next-modules.d.ts +16 -0
- package/src/next/pages.tsx +80 -0
- package/src/next/route.ts +24 -0
- package/src/options.ts +58 -0
- package/src/schema.ts +19 -0
- package/src/server/endpoint.ts +141 -0
- package/src/server/health.ts +12 -0
- package/src/server/index.ts +36 -0
- package/src/server/options.ts +54 -0
- package/src/server/privacy.ts +49 -0
- package/src/server/tokens.ts +188 -0
- package/src/token-status.ts +27 -0
- package/src/ui/index.ts +11 -0
- package/src/ui/token-manager.tsx +254 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { formatMessage, type Locale } from "@softure-ai/core";
|
|
4
|
+
import { Button, ButtonLink, Checkbox, type ClassNames, createSlotClassGetter, FormError, TextField } from "@softure-ai/ui";
|
|
5
|
+
import { useActionState, useState } from "react";
|
|
6
|
+
import type { IssuedToken, IssueTokenFormState, RevokeTokenFormState } from "../contract.js";
|
|
7
|
+
import { getTokenErrorMessage, type McpAccessMessages } from "../messages/index.js";
|
|
8
|
+
import { MAX_TOKEN_NAME_LENGTH } from "../options.js";
|
|
9
|
+
|
|
10
|
+
// The token page's client part: the tool catalog, the issue form, the token just issued with its
|
|
11
|
+
// setup snippets, and the owner's tokens with a revoke button each. Both forms submit to their
|
|
12
|
+
// server actions through `useActionState`. The plaintext lives only in the issue form's state:
|
|
13
|
+
// "Done" or leaving the page drops it. Copy comes from the module's messages; styling only from
|
|
14
|
+
// @softure-ai/ui classes.
|
|
15
|
+
|
|
16
|
+
export type IssueTokenAction = (previous: IssueTokenFormState, formData: FormData) => Promise<IssueTokenFormState>;
|
|
17
|
+
export type RevokeTokenAction = (previous: RevokeTokenFormState, formData: FormData) => Promise<RevokeTokenFormState>;
|
|
18
|
+
|
|
19
|
+
export type TokenManagerSlot = "root" | "section" | "heading" | "text" | "list" | "item" | "itemHeader" | "badge" | "details" | "snippet" | "code";
|
|
20
|
+
|
|
21
|
+
/** One tool of the app's catalog, prepared on the server. */
|
|
22
|
+
export interface TokenManagerTool {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly description: string;
|
|
25
|
+
/** "Reads", "Changes data", or that writes are off, already in the app's copy. */
|
|
26
|
+
readonly accessText: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One of the owner's tokens, prepared on the server. */
|
|
30
|
+
export interface TokenManagerRow {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly name: string;
|
|
33
|
+
/** Read only, or read and change, already in the app's copy. */
|
|
34
|
+
readonly scopeText: string;
|
|
35
|
+
/** Active, expiring in n days, or expired. */
|
|
36
|
+
readonly statusText: string;
|
|
37
|
+
readonly isExpired: boolean;
|
|
38
|
+
/** Created, valid until and last used, already formatted. */
|
|
39
|
+
readonly details: readonly string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TokenManagerProps {
|
|
43
|
+
readonly tools: readonly TokenManagerTool[];
|
|
44
|
+
readonly tokens: readonly TokenManagerRow[];
|
|
45
|
+
/** Offers write tokens; off, every token reads only. */
|
|
46
|
+
readonly allowWrites: boolean;
|
|
47
|
+
/** The per-account limit, for the form's note. */
|
|
48
|
+
readonly maxTokens: number;
|
|
49
|
+
readonly issueAction: IssueTokenAction;
|
|
50
|
+
readonly revokeAction: RevokeTokenAction;
|
|
51
|
+
readonly messages: McpAccessMessages;
|
|
52
|
+
/** Locale of the built-in copy of the ui primitives. */
|
|
53
|
+
readonly locale?: Locale;
|
|
54
|
+
readonly classNames?: ClassNames<TokenManagerSlot>;
|
|
55
|
+
readonly unstyled?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DEFAULT_CLASSES: Readonly<Record<TokenManagerSlot, string>> = {
|
|
59
|
+
root: "sft:flex sft:flex-col sft:gap-4 sft:font-sans",
|
|
60
|
+
section: "sft:flex sft:flex-col sft:gap-3 sft:border-t sft:border-border sft:pt-4 sft:first-of-type:border-t-0 sft:first-of-type:pt-0",
|
|
61
|
+
heading: "sft:m-0 sft:text-base sft:font-semibold sft:text-foreground",
|
|
62
|
+
text: "sft:m-0 sft:text-sm sft:text-muted",
|
|
63
|
+
list: "sft:m-0 sft:flex sft:list-none sft:flex-col sft:gap-3 sft:p-0",
|
|
64
|
+
item: "sft:flex sft:flex-col sft:gap-1",
|
|
65
|
+
itemHeader: "sft:flex sft:items-center sft:justify-between sft:gap-2",
|
|
66
|
+
badge: "sft:text-xs sft:font-medium sft:text-muted",
|
|
67
|
+
details: "sft:m-0 sft:text-xs sft:text-muted",
|
|
68
|
+
snippet: "sft:flex sft:flex-col sft:gap-1.5",
|
|
69
|
+
code: "sft:m-0 sft:block sft:overflow-x-auto sft:whitespace-normal sft:break-words sft:rounded-control sft:border sft:border-border sft:bg-surface-raised sft:p-4 sft:font-mono sft:text-xs sft:text-foreground",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type Slot = (part: TokenManagerSlot) => string | undefined;
|
|
73
|
+
|
|
74
|
+
/** A labelled snippet with a copy button. */
|
|
75
|
+
function Snippet({ title, value, messages, slot, unstyled }: { title: string; value: string; messages: McpAccessMessages; slot: Slot; unstyled: boolean | undefined }) {
|
|
76
|
+
const [isCopied, setIsCopied] = useState(false);
|
|
77
|
+
function copy() {
|
|
78
|
+
void navigator.clipboard.writeText(value).then(
|
|
79
|
+
() => setIsCopied(true),
|
|
80
|
+
// Clipboard access can be refused; the text stays on screen to select by hand.
|
|
81
|
+
() => setIsCopied(false),
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return (
|
|
85
|
+
<div className={slot("snippet")}>
|
|
86
|
+
<div className={slot("itemHeader")}>
|
|
87
|
+
<p className={slot("heading")}>{title}</p>
|
|
88
|
+
<Button variant="secondary" size="sm" onClick={copy} unstyled={unstyled}>
|
|
89
|
+
{isCopied ? messages.issued.copied : messages.issued.copy}
|
|
90
|
+
</Button>
|
|
91
|
+
</div>
|
|
92
|
+
<pre className={slot("code")}>{value}</pre>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function IssuedTokenPanel({
|
|
98
|
+
issued,
|
|
99
|
+
onDone,
|
|
100
|
+
messages,
|
|
101
|
+
slot,
|
|
102
|
+
unstyled,
|
|
103
|
+
}: {
|
|
104
|
+
issued: IssuedToken;
|
|
105
|
+
onDone: () => void;
|
|
106
|
+
messages: McpAccessMessages;
|
|
107
|
+
slot: Slot;
|
|
108
|
+
unstyled: boolean | undefined;
|
|
109
|
+
}) {
|
|
110
|
+
const snippet = (title: string, value: string) => <Snippet title={title} value={value} messages={messages} slot={slot} unstyled={unstyled} />;
|
|
111
|
+
return (
|
|
112
|
+
<section className={slot("section")}>
|
|
113
|
+
<h3 className={slot("heading")}>{formatMessage(messages.issued.title, { name: issued.name })}</h3>
|
|
114
|
+
<p className={slot("text")}>{messages.issued.once}</p>
|
|
115
|
+
<p className={slot("text")}>
|
|
116
|
+
{issued.expiresText} {issued.canWrite ? messages.issued.readWrite : messages.issued.readOnly}
|
|
117
|
+
</p>
|
|
118
|
+
{snippet(messages.issued.promptTitle, issued.setup.assistantPrompt)}
|
|
119
|
+
<p className={slot("text")}>{messages.issued.promptHint}</p>
|
|
120
|
+
<ButtonLink href={issued.setup.claudeCodeLink} variant="secondary" size="sm" unstyled={unstyled}>
|
|
121
|
+
{messages.issued.openClaudeCode}
|
|
122
|
+
</ButtonLink>
|
|
123
|
+
{snippet(messages.issued.commandTitle, issued.setup.claudeCodeCommand)}
|
|
124
|
+
{snippet(messages.issued.jsonTitle, issued.setup.jsonConfig)}
|
|
125
|
+
{snippet(messages.issued.desktopTitle, issued.setup.desktopConfig)}
|
|
126
|
+
{snippet(messages.issued.headerTitle, `${issued.setup.endpointUrl}\nAuthorization: ${issued.setup.authorizationHeader}`)}
|
|
127
|
+
<Button variant="primary" onClick={onDone} unstyled={unstyled}>
|
|
128
|
+
{messages.issued.done}
|
|
129
|
+
</Button>
|
|
130
|
+
</section>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function IssueForm({
|
|
135
|
+
action,
|
|
136
|
+
allowWrites,
|
|
137
|
+
maxTokens,
|
|
138
|
+
messages,
|
|
139
|
+
locale,
|
|
140
|
+
slot,
|
|
141
|
+
unstyled,
|
|
142
|
+
}: {
|
|
143
|
+
action: IssueTokenAction;
|
|
144
|
+
allowWrites: boolean;
|
|
145
|
+
maxTokens: number;
|
|
146
|
+
messages: McpAccessMessages;
|
|
147
|
+
locale: Locale | undefined;
|
|
148
|
+
slot: Slot;
|
|
149
|
+
unstyled: boolean | undefined;
|
|
150
|
+
}) {
|
|
151
|
+
const [state, formAction, isPending] = useActionState(action, { status: "idle" });
|
|
152
|
+
// The id of the issued token the owner dismissed; a new one shows again.
|
|
153
|
+
const [dismissedId, setDismissedId] = useState<string | null>(null);
|
|
154
|
+
const issued = state.status === "ok" && state.issued.id !== dismissedId ? state.issued : null;
|
|
155
|
+
return (
|
|
156
|
+
<>
|
|
157
|
+
<section className={slot("section")}>
|
|
158
|
+
<h2 className={slot("heading")}>{messages.issue.title}</h2>
|
|
159
|
+
<form action={formAction} className={slot("list")}>
|
|
160
|
+
<TextField
|
|
161
|
+
name="name"
|
|
162
|
+
label={messages.issue.name}
|
|
163
|
+
hint={messages.issue.nameHint}
|
|
164
|
+
maxLength={MAX_TOKEN_NAME_LENGTH}
|
|
165
|
+
required
|
|
166
|
+
autoComplete="off"
|
|
167
|
+
locale={locale}
|
|
168
|
+
unstyled={unstyled}
|
|
169
|
+
/>
|
|
170
|
+
{allowWrites ? <Checkbox name="canWrite" label={messages.issue.canWrite} description={messages.issue.canWriteHint} unstyled={unstyled} /> : null}
|
|
171
|
+
<p className={slot("text")}>{formatMessage(messages.issue.limit, { max: maxTokens })}</p>
|
|
172
|
+
<FormError message={state.status === "error" ? getTokenErrorMessage(messages, state.error) : undefined} unstyled={unstyled} />
|
|
173
|
+
<Button type="submit" variant="primary" pending={isPending} unstyled={unstyled}>
|
|
174
|
+
{isPending ? messages.issue.pending : messages.issue.submit}
|
|
175
|
+
</Button>
|
|
176
|
+
</form>
|
|
177
|
+
</section>
|
|
178
|
+
{issued === null ? null : <IssuedTokenPanel issued={issued} onDone={() => setDismissedId(issued.id)} messages={messages} slot={slot} unstyled={unstyled} />}
|
|
179
|
+
</>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function TokenItem({
|
|
184
|
+
row,
|
|
185
|
+
action,
|
|
186
|
+
messages,
|
|
187
|
+
slot,
|
|
188
|
+
unstyled,
|
|
189
|
+
}: {
|
|
190
|
+
row: TokenManagerRow;
|
|
191
|
+
action: RevokeTokenAction;
|
|
192
|
+
messages: McpAccessMessages;
|
|
193
|
+
slot: Slot;
|
|
194
|
+
unstyled: boolean | undefined;
|
|
195
|
+
}) {
|
|
196
|
+
const [state, formAction, isPending] = useActionState(action, { status: "idle" });
|
|
197
|
+
return (
|
|
198
|
+
<li className={slot("item")} data-token-id={row.id}>
|
|
199
|
+
<div className={slot("itemHeader")}>
|
|
200
|
+
<p className={slot("heading")}>{row.name}</p>
|
|
201
|
+
<form action={formAction}>
|
|
202
|
+
<input type="hidden" name="id" value={row.id} />
|
|
203
|
+
<Button type="submit" variant="danger" size="sm" pending={isPending} aria-label={`${messages.list.revoke}: ${row.name}`} unstyled={unstyled}>
|
|
204
|
+
{isPending ? messages.list.revoking : messages.list.revoke}
|
|
205
|
+
</Button>
|
|
206
|
+
</form>
|
|
207
|
+
</div>
|
|
208
|
+
<p className={row.isExpired ? slot("details") : slot("badge")}>
|
|
209
|
+
{row.statusText} · {row.scopeText}
|
|
210
|
+
</p>
|
|
211
|
+
<p className={slot("details")}>{row.details.join(" · ")}</p>
|
|
212
|
+
<FormError message={state.status === "error" ? getTokenErrorMessage(messages, state.error) : undefined} unstyled={unstyled} />
|
|
213
|
+
</li>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function TokenManager({ tools, tokens, allowWrites, maxTokens, issueAction, revokeAction, messages, locale, classNames, unstyled }: TokenManagerProps) {
|
|
218
|
+
const slot = createSlotClassGetter({ defaults: DEFAULT_CLASSES, classNames, unstyled });
|
|
219
|
+
return (
|
|
220
|
+
<div className={slot("root")}>
|
|
221
|
+
<section className={slot("section")}>
|
|
222
|
+
<h2 className={slot("heading")}>{messages.tools.title}</h2>
|
|
223
|
+
{tools.length === 0 ? (
|
|
224
|
+
<p className={slot("text")}>{messages.tools.empty}</p>
|
|
225
|
+
) : (
|
|
226
|
+
<ul className={slot("list")}>
|
|
227
|
+
{tools.map((tool) => (
|
|
228
|
+
<li key={tool.name} className={slot("item")}>
|
|
229
|
+
<div className={slot("itemHeader")}>
|
|
230
|
+
<code className={slot("heading")}>{tool.name}</code>
|
|
231
|
+
<span className={slot("badge")}>{tool.accessText}</span>
|
|
232
|
+
</div>
|
|
233
|
+
<p className={slot("text")}>{tool.description}</p>
|
|
234
|
+
</li>
|
|
235
|
+
))}
|
|
236
|
+
</ul>
|
|
237
|
+
)}
|
|
238
|
+
</section>
|
|
239
|
+
<IssueForm action={issueAction} allowWrites={allowWrites} maxTokens={maxTokens} messages={messages} locale={locale} slot={slot} unstyled={unstyled} />
|
|
240
|
+
<section className={slot("section")}>
|
|
241
|
+
<h2 className={slot("heading")}>{messages.list.title}</h2>
|
|
242
|
+
{tokens.length === 0 ? (
|
|
243
|
+
<p className={slot("text")}>{messages.list.empty}</p>
|
|
244
|
+
) : (
|
|
245
|
+
<ul className={slot("list")}>
|
|
246
|
+
{tokens.map((row) => (
|
|
247
|
+
<TokenItem key={row.id} row={row} action={revokeAction} messages={messages} slot={slot} unstyled={unstyled} />
|
|
248
|
+
))}
|
|
249
|
+
</ul>
|
|
250
|
+
)}
|
|
251
|
+
</section>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|