@softure-ai/mcp-access 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +110 -21
- package/adoption/move-app-tables.sql +100 -0
- package/dist/contract.d.ts +13 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/index.d.ts +51 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -5
- package/dist/index.js.map +1 -1
- package/dist/messages/en.d.ts +28 -0
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +28 -0
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/index.d.ts +56 -0
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/messages/pl.d.ts.map +1 -1
- package/dist/messages/pl.js +28 -0
- package/dist/messages/pl.js.map +1 -1
- package/dist/next/actions.d.ts +3 -1
- package/dist/next/actions.d.ts.map +1 -1
- package/dist/next/actions.js +19 -0
- package/dist/next/actions.js.map +1 -1
- package/dist/next/context.js +2 -2
- package/dist/next/context.js.map +1 -1
- package/dist/next/index.d.ts +3 -1
- package/dist/next/index.d.ts.map +1 -1
- package/dist/next/index.js +4 -2
- package/dist/next/index.js.map +1 -1
- package/dist/next/oauth-page.d.ts +7 -0
- package/dist/next/oauth-page.d.ts.map +1 -0
- package/dist/next/oauth-page.js +35 -0
- package/dist/next/oauth-page.js.map +1 -0
- package/dist/next/oauth-routes.d.ts +16 -0
- package/dist/next/oauth-routes.d.ts.map +1 -0
- package/dist/next/oauth-routes.js +50 -0
- package/dist/next/oauth-routes.js.map +1 -0
- package/dist/next/pages.d.ts.map +1 -1
- package/dist/next/pages.js +13 -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 +28 -1
- package/dist/options.js.map +1 -1
- package/dist/schema.d.ts +486 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +45 -3
- package/dist/schema.js.map +1 -1
- package/dist/server/endpoint.d.ts +2 -4
- package/dist/server/endpoint.d.ts.map +1 -1
- package/dist/server/endpoint.js +10 -7
- package/dist/server/endpoint.js.map +1 -1
- package/dist/server/health.d.ts.map +1 -1
- package/dist/server/health.js +5 -3
- package/dist/server/health.js.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/oauth-http.d.ts +113 -0
- package/dist/server/oauth-http.d.ts.map +1 -0
- package/dist/server/oauth-http.js +374 -0
- package/dist/server/oauth-http.js.map +1 -0
- package/dist/server/oauth-validation.d.ts +28 -0
- package/dist/server/oauth-validation.d.ts.map +1 -0
- package/dist/server/oauth-validation.js +88 -0
- package/dist/server/oauth-validation.js.map +1 -0
- package/dist/server/oauth.d.ts +106 -0
- package/dist/server/oauth.d.ts.map +1 -0
- package/dist/server/oauth.js +235 -0
- package/dist/server/oauth.js.map +1 -0
- package/dist/server/options.d.ts +8 -0
- package/dist/server/options.d.ts.map +1 -1
- package/dist/server/options.js +8 -1
- package/dist/server/options.js.map +1 -1
- package/dist/server/pkce.d.ts +7 -0
- package/dist/server/pkce.d.ts.map +1 -0
- package/dist/server/pkce.js +24 -0
- package/dist/server/pkce.js.map +1 -0
- package/dist/server/privacy.d.ts +7 -0
- package/dist/server/privacy.d.ts.map +1 -1
- package/dist/server/privacy.js +19 -9
- package/dist/server/privacy.js.map +1 -1
- package/dist/server/scopes.d.ts +5 -0
- package/dist/server/scopes.d.ts.map +1 -0
- package/dist/server/scopes.js +6 -0
- package/dist/server/scopes.js.map +1 -0
- package/dist/server/tokens.d.ts +18 -6
- package/dist/server/tokens.d.ts.map +1 -1
- package/dist/server/tokens.js +60 -25
- package/dist/server/tokens.js.map +1 -1
- package/dist/ui/consent-form.d.ts +31 -0
- package/dist/ui/consent-form.d.ts.map +1 -0
- package/dist/ui/consent-form.js +24 -0
- package/dist/ui/consent-form.js.map +1 -0
- package/dist/ui/index.d.ts +2 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/token-manager.d.ts +16 -2
- package/dist/ui/token-manager.d.ts.map +1 -1
- package/dist/ui/token-manager.js +6 -2
- package/dist/ui/token-manager.js.map +1 -1
- package/migrations/0002_create_oauth_grants.sql +68 -0
- package/module.json +18 -4
- package/package.json +7 -2
- package/src/contract.ts +16 -1
- package/src/index.ts +28 -4
- package/src/messages/en.ts +28 -0
- package/src/messages/pl.ts +28 -0
- package/src/next/actions.ts +19 -1
- package/src/next/context.ts +2 -2
- package/src/next/index.ts +11 -2
- package/src/next/oauth-page.tsx +67 -0
- package/src/next/oauth-routes.ts +65 -0
- package/src/next/pages.tsx +16 -3
- package/src/options.ts +32 -1
- package/src/schema.ts +51 -3
- package/src/server/endpoint.ts +10 -7
- package/src/server/health.ts +5 -3
- package/src/server/index.ts +50 -0
- package/src/server/oauth-http.ts +431 -0
- package/src/server/oauth-validation.ts +106 -0
- package/src/server/oauth.ts +315 -0
- package/src/server/options.ts +16 -1
- package/src/server/pkce.ts +27 -0
- package/src/server/privacy.ts +26 -9
- package/src/server/scopes.ts +6 -0
- package/src/server/tokens.ts +62 -25
- package/src/ui/consent-form.tsx +103 -0
- package/src/ui/index.ts +3 -0
- package/src/ui/token-manager.tsx +79 -3
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { formatMessage } from "@softure-ai/core";
|
|
2
|
+
import { Button, Checkbox, type ClassNames, createSlotClassGetter } from "@softure-ai/ui";
|
|
3
|
+
import type { McpAccessMessages } from "../messages/index.js";
|
|
4
|
+
|
|
5
|
+
// The OAuth consent screen's body: which app (its registered name and where the code goes, which
|
|
6
|
+
// the app cannot fake), which account (two accounts in one browser happen) and what access (read
|
|
7
|
+
// always, change only when the client asked, the app allows it and the person ticks it). A plain
|
|
8
|
+
// HTML form posting to the decision route, so the `303` to the client works without JavaScript and
|
|
9
|
+
// with native app schemes. Server-safe: no hooks.
|
|
10
|
+
|
|
11
|
+
export type ConsentFormSlot = "root" | "details" | "term" | "value" | "note" | "actions" | "text" | "link";
|
|
12
|
+
|
|
13
|
+
export interface ConsentFormProps {
|
|
14
|
+
/** The decision route the form posts to. */
|
|
15
|
+
readonly action: string;
|
|
16
|
+
/** The authorization request's parameters, carried in hidden fields. */
|
|
17
|
+
readonly params: readonly (readonly [string, string])[];
|
|
18
|
+
readonly clientName: string;
|
|
19
|
+
/** Where the code goes: the host for http(s), the whole URI for a native scheme. */
|
|
20
|
+
readonly redirectTarget: string;
|
|
21
|
+
readonly accountEmail: string;
|
|
22
|
+
/** Offer the change checkbox: the client asked for `mcp:write` and the app allows writes. */
|
|
23
|
+
readonly isWriteOffered: boolean;
|
|
24
|
+
readonly messages: McpAccessMessages;
|
|
25
|
+
readonly classNames?: ClassNames<ConsentFormSlot>;
|
|
26
|
+
readonly unstyled?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const DEFAULT_CLASSES: Readonly<Record<ConsentFormSlot, string>> = {
|
|
30
|
+
root: "sft:flex sft:flex-col sft:gap-4 sft:font-sans",
|
|
31
|
+
details: "sft:m-0 sft:flex sft:flex-col sft:gap-3 sft:text-sm",
|
|
32
|
+
term: "sft:text-muted",
|
|
33
|
+
value: "sft:m-0 sft:font-medium sft:text-foreground",
|
|
34
|
+
note: "sft:font-normal sft:text-muted",
|
|
35
|
+
actions: "sft:flex sft:flex-col sft:gap-3",
|
|
36
|
+
text: "sft:m-0 sft:text-sm sft:text-muted",
|
|
37
|
+
link: "sft:font-medium sft:text-accent",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function ConsentForm({ action, params, clientName, redirectTarget, accountEmail, isWriteOffered, messages, classNames, unstyled }: ConsentFormProps) {
|
|
41
|
+
const slot = createSlotClassGetter({ defaults: DEFAULT_CLASSES, classNames, unstyled });
|
|
42
|
+
const copy = messages.consent;
|
|
43
|
+
return (
|
|
44
|
+
<form method="post" action={action} className={slot("root")}>
|
|
45
|
+
{params.map(([name, value]) => (
|
|
46
|
+
<input key={name} type="hidden" name={name} value={value} />
|
|
47
|
+
))}
|
|
48
|
+
<dl className={slot("details")}>
|
|
49
|
+
<div>
|
|
50
|
+
<dt className={slot("term")}>{copy.app}</dt>
|
|
51
|
+
<dd className={slot("value")}>
|
|
52
|
+
{clientName} <span className={slot("note")}>({formatMessage(copy.returnsTo, { target: redirectTarget })})</span>
|
|
53
|
+
</dd>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
<dt className={slot("term")}>{copy.account}</dt>
|
|
57
|
+
<dd className={slot("value")}>{accountEmail}</dd>
|
|
58
|
+
</div>
|
|
59
|
+
<div>
|
|
60
|
+
<dt className={slot("term")}>{copy.access}</dt>
|
|
61
|
+
<dd className={slot("value")}>{copy.readAccess}</dd>
|
|
62
|
+
</div>
|
|
63
|
+
</dl>
|
|
64
|
+
{isWriteOffered ? <Checkbox name="canWrite" label={copy.allowWrite} description={copy.allowWriteHint} unstyled={unstyled} /> : null}
|
|
65
|
+
<div className={slot("actions")}>
|
|
66
|
+
<Button type="submit" name="decision" value="allow" variant="primary" size="lg" fullWidth unstyled={unstyled}>
|
|
67
|
+
{copy.allow}
|
|
68
|
+
</Button>
|
|
69
|
+
<Button type="submit" name="decision" value="deny" variant="secondary" size="lg" fullWidth unstyled={unstyled}>
|
|
70
|
+
{copy.deny}
|
|
71
|
+
</Button>
|
|
72
|
+
</div>
|
|
73
|
+
</form>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ConsentErrorProps {
|
|
78
|
+
readonly message: string;
|
|
79
|
+
/** The client's redirect URI with the error, when the client was confirmed; a link, never automatic. */
|
|
80
|
+
readonly backLocation?: string;
|
|
81
|
+
readonly backTarget?: string;
|
|
82
|
+
readonly messages: McpAccessMessages;
|
|
83
|
+
readonly classNames?: ClassNames<ConsentFormSlot>;
|
|
84
|
+
readonly unstyled?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Why the request cannot go on. Nothing was granted; going back to the client is the person's click. */
|
|
88
|
+
export function ConsentError({ message, backLocation, backTarget, messages, classNames, unstyled }: ConsentErrorProps) {
|
|
89
|
+
const slot = createSlotClassGetter({ defaults: DEFAULT_CLASSES, classNames, unstyled });
|
|
90
|
+
return (
|
|
91
|
+
<div className={slot("root")}>
|
|
92
|
+
<p className={slot("text")}>{message}</p>
|
|
93
|
+
<p className={slot("text")}>{messages.consent.nothingGranted}</p>
|
|
94
|
+
{backLocation === undefined || backTarget === undefined ? null : (
|
|
95
|
+
<p className={slot("text")}>
|
|
96
|
+
<a href={backLocation} className={slot("link")}>
|
|
97
|
+
{formatMessage(messages.consent.backToApp, { target: backTarget })}
|
|
98
|
+
</a>
|
|
99
|
+
</p>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
package/src/ui/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// The token manager, to compose into the app's own page. It takes the server actions from
|
|
2
2
|
// `@softure-ai/mcp-access/next` as props: no `next/*` import here (docs/02-module-standard.md §5).
|
|
3
|
+
export { ConsentError, ConsentForm, type ConsentErrorProps, type ConsentFormProps, type ConsentFormSlot } from "./consent-form.js";
|
|
3
4
|
export {
|
|
4
5
|
TokenManager,
|
|
6
|
+
type RevokeGrantAction,
|
|
7
|
+
type TokenManagerGrant,
|
|
5
8
|
type IssueTokenAction,
|
|
6
9
|
type RevokeTokenAction,
|
|
7
10
|
type TokenManagerProps,
|
package/src/ui/token-manager.tsx
CHANGED
|
@@ -3,18 +3,20 @@
|
|
|
3
3
|
import { formatMessage, type Locale } from "@softure-ai/core";
|
|
4
4
|
import { Button, ButtonLink, Checkbox, type ClassNames, createSlotClassGetter, FormError, TextField } from "@softure-ai/ui";
|
|
5
5
|
import { useActionState, useState } from "react";
|
|
6
|
-
import type { IssuedToken, IssueTokenFormState, RevokeTokenFormState } from "../contract.js";
|
|
6
|
+
import type { IssuedToken, IssueTokenFormState, RevokeGrantFormState, RevokeTokenFormState } from "../contract.js";
|
|
7
7
|
import { getTokenErrorMessage, type McpAccessMessages } from "../messages/index.js";
|
|
8
8
|
import { MAX_TOKEN_NAME_LENGTH } from "../options.js";
|
|
9
9
|
|
|
10
10
|
// The token page's client part: the tool catalog, the issue form, the token just issued with its
|
|
11
|
-
// setup snippets,
|
|
11
|
+
// setup snippets, the owner's tokens with a revoke button each, and, with OAuth on, the apps
|
|
12
|
+
// connected through OAuth with a disconnect button each. Both forms submit to their
|
|
12
13
|
// server actions through `useActionState`. The plaintext lives only in the issue form's state:
|
|
13
14
|
// "Done" or leaving the page drops it. Copy comes from the module's messages; styling only from
|
|
14
15
|
// @softure-ai/ui classes.
|
|
15
16
|
|
|
16
17
|
export type IssueTokenAction = (previous: IssueTokenFormState, formData: FormData) => Promise<IssueTokenFormState>;
|
|
17
18
|
export type RevokeTokenAction = (previous: RevokeTokenFormState, formData: FormData) => Promise<RevokeTokenFormState>;
|
|
19
|
+
export type RevokeGrantAction = (previous: RevokeGrantFormState, formData: FormData) => Promise<RevokeGrantFormState>;
|
|
18
20
|
|
|
19
21
|
export type TokenManagerSlot = "root" | "section" | "heading" | "text" | "list" | "item" | "itemHeader" | "badge" | "details" | "snippet" | "code";
|
|
20
22
|
|
|
@@ -39,6 +41,16 @@ export interface TokenManagerRow {
|
|
|
39
41
|
readonly details: readonly string[];
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
/** One app connected through OAuth, prepared on the server. */
|
|
45
|
+
export interface TokenManagerGrant {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly clientName: string;
|
|
48
|
+
/** Read only, or read and change, already in the app's copy. */
|
|
49
|
+
readonly scopeText: string;
|
|
50
|
+
/** Connected on and last used, already formatted. */
|
|
51
|
+
readonly details: readonly string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
export interface TokenManagerProps {
|
|
43
55
|
readonly tools: readonly TokenManagerTool[];
|
|
44
56
|
readonly tokens: readonly TokenManagerRow[];
|
|
@@ -48,6 +60,10 @@ export interface TokenManagerProps {
|
|
|
48
60
|
readonly maxTokens: number;
|
|
49
61
|
readonly issueAction: IssueTokenAction;
|
|
50
62
|
readonly revokeAction: RevokeTokenAction;
|
|
63
|
+
/** The connected apps; leave out while OAuth is off, and the section is not shown. */
|
|
64
|
+
readonly grants?: readonly TokenManagerGrant[];
|
|
65
|
+
/** Disconnects one app; needed with `grants`. */
|
|
66
|
+
readonly revokeGrantAction?: RevokeGrantAction;
|
|
51
67
|
readonly messages: McpAccessMessages;
|
|
52
68
|
/** Locale of the built-in copy of the ui primitives. */
|
|
53
69
|
readonly locale?: Locale;
|
|
@@ -214,7 +230,52 @@ function TokenItem({
|
|
|
214
230
|
);
|
|
215
231
|
}
|
|
216
232
|
|
|
217
|
-
|
|
233
|
+
function GrantItem({
|
|
234
|
+
grant,
|
|
235
|
+
action,
|
|
236
|
+
messages,
|
|
237
|
+
slot,
|
|
238
|
+
unstyled,
|
|
239
|
+
}: {
|
|
240
|
+
grant: TokenManagerGrant;
|
|
241
|
+
action: RevokeGrantAction;
|
|
242
|
+
messages: McpAccessMessages;
|
|
243
|
+
slot: Slot;
|
|
244
|
+
unstyled: boolean | undefined;
|
|
245
|
+
}) {
|
|
246
|
+
const [state, formAction, isPending] = useActionState(action, { status: "idle" });
|
|
247
|
+
return (
|
|
248
|
+
<li className={slot("item")} data-grant-id={grant.id}>
|
|
249
|
+
<div className={slot("itemHeader")}>
|
|
250
|
+
<p className={slot("heading")}>{grant.clientName}</p>
|
|
251
|
+
<form action={formAction}>
|
|
252
|
+
<input type="hidden" name="id" value={grant.id} />
|
|
253
|
+
<Button type="submit" variant="danger" size="sm" pending={isPending} aria-label={`${messages.grants.disconnect}: ${grant.clientName}`} unstyled={unstyled}>
|
|
254
|
+
{isPending ? messages.grants.disconnecting : messages.grants.disconnect}
|
|
255
|
+
</Button>
|
|
256
|
+
</form>
|
|
257
|
+
</div>
|
|
258
|
+
<p className={slot("badge")}>{grant.scopeText}</p>
|
|
259
|
+
<p className={slot("details")}>{grant.details.join(" · ")}</p>
|
|
260
|
+
<FormError message={state.status === "error" ? getTokenErrorMessage(messages, state.error) : undefined} unstyled={unstyled} />
|
|
261
|
+
</li>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function TokenManager({
|
|
266
|
+
tools,
|
|
267
|
+
tokens,
|
|
268
|
+
allowWrites,
|
|
269
|
+
maxTokens,
|
|
270
|
+
issueAction,
|
|
271
|
+
revokeAction,
|
|
272
|
+
grants,
|
|
273
|
+
revokeGrantAction,
|
|
274
|
+
messages,
|
|
275
|
+
locale,
|
|
276
|
+
classNames,
|
|
277
|
+
unstyled,
|
|
278
|
+
}: TokenManagerProps) {
|
|
218
279
|
const slot = createSlotClassGetter({ defaults: DEFAULT_CLASSES, classNames, unstyled });
|
|
219
280
|
return (
|
|
220
281
|
<div className={slot("root")}>
|
|
@@ -249,6 +310,21 @@ export function TokenManager({ tools, tokens, allowWrites, maxTokens, issueActio
|
|
|
249
310
|
</ul>
|
|
250
311
|
)}
|
|
251
312
|
</section>
|
|
313
|
+
{grants === undefined || revokeGrantAction === undefined ? null : (
|
|
314
|
+
<section className={slot("section")}>
|
|
315
|
+
<h2 className={slot("heading")}>{messages.grants.title}</h2>
|
|
316
|
+
<p className={slot("text")}>{messages.grants.lead}</p>
|
|
317
|
+
{grants.length === 0 ? (
|
|
318
|
+
<p className={slot("text")}>{messages.grants.empty}</p>
|
|
319
|
+
) : (
|
|
320
|
+
<ul className={slot("list")}>
|
|
321
|
+
{grants.map((grant) => (
|
|
322
|
+
<GrantItem key={grant.id} grant={grant} action={revokeGrantAction} messages={messages} slot={slot} unstyled={unstyled} />
|
|
323
|
+
))}
|
|
324
|
+
</ul>
|
|
325
|
+
)}
|
|
326
|
+
</section>
|
|
327
|
+
)}
|
|
252
328
|
</div>
|
|
253
329
|
);
|
|
254
330
|
}
|