diff --git a/mcp/src/pages/connections-page.tsx b/mcp/src/pages/connections-page.tsx
index 64ee55269c..626702cb25 100644
--- a/mcp/src/pages/connections-page.tsx
+++ b/mcp/src/pages/connections-page.tsx
@@ -2,8 +2,9 @@ import { isValidElement } from "hono/jsx";
/**
* `/connections` — where a user sees which AI apps hold a grant to their Artcraft account and
- * disconnects them. The app itself has no sessions screen, so this page is the only place that
- * can show "Claude is connected". A pure function of its view model; everything is escaped.
+ * disconnects them, and where they mint personal tokens for integrations that cannot run the
+ * OAuth flow themselves. The app itself has no sessions screen, so this page is the only place
+ * that can show "Claude is connected". A pure function of its view model; everything is escaped.
*/
export interface ConnectionView {
@@ -13,6 +14,21 @@ export interface ConnectionView {
readonly createdAt: string;
}
+export interface PersonalTokenView {
+ readonly id: string;
+ readonly label: string;
+ readonly hint: string;
+ readonly createdAt: string;
+ readonly expiresAt: string;
+}
+
+/** Shown exactly once, in the response to the request that created it. */
+export interface NewTokenView {
+ readonly label: string;
+ readonly secret: string;
+ readonly expiresAt: string;
+}
+
export interface ConnectionsView {
readonly csrfToken: string;
readonly scriptNonce: string;
@@ -24,6 +40,8 @@ export interface ConnectionsView {
readonly signedIn?: {
readonly username: string;
readonly connections: readonly ConnectionView[];
+ readonly personalTokens: readonly PersonalTokenView[];
+ readonly newToken?: NewTokenView;
};
}
@@ -34,6 +52,9 @@ export const CONNECTIONS_FORM_FIELDS = {
password: "password",
googleCredential: "google_credential",
grantId: "grant_id",
+ tokenLabel: "token_label",
+ tokenLifetime: "token_lifetime",
+ tokenId: "token_id",
} as const;
export const CONNECTIONS_PATHS = {
@@ -41,8 +62,14 @@ export const CONNECTIONS_PATHS = {
signIn: "/connections/sign-in",
signOut: "/connections/sign-out",
revoke: "/connections/revoke",
+ createToken: "/connections/tokens/create",
+ revokeToken: "/connections/tokens/revoke",
} as const;
+/** The lifetimes a user can pick, in days; the form value is the key. */
+export const TOKEN_LIFETIME_DAYS = ["30", "90"] as const;
+export type TokenLifetimeDays = (typeof TOKEN_LIFETIME_DAYS)[number];
+
export function renderConnectionsPage(view: ConnectionsView): string {
const element: unknown =