From 852316b31af27940cbaeec2f4dd51a5eb0650a0e Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:06:21 +0000 Subject: [PATCH 1/4] Support credential field labels --- bun.lock | 4 ++-- package.json | 2 +- src/lib/mcp/tools/vault-credential-flow.test.ts | 16 ++++++++++++++++ src/lib/mcp/tools/vault-credentials.ts | 14 +++++++++++++- src/lib/mcp/vault-responses.ts | 5 +++-- src/lib/mcp/vault-steering.test.ts | 11 +++++++++-- 6 files changed, 44 insertions(+), 8 deletions(-) diff --git a/bun.lock b/bun.lock index 99b8815..05af7c4 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "@clerk/themes": "^2.4.19", "@modelcontextprotocol/sdk": "1.26.0", "@onkernel/managed-auth-react": "0.5.3", - "@onkernel/sdk": "^0.109.0", + "@onkernel/sdk": "github:kernel/kernel-node-sdk#555797f46b7b77f2d7c9f45fe663a4f7e8ffe981", "@posthog/mcp": "0.10.1", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", @@ -154,7 +154,7 @@ "@onkernel/managed-auth-react": ["@onkernel/managed-auth-react@0.5.3", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5Ps7h7HknAxL4ZLnJCzsLhW8MLRr0SH1P8+eALBZXCmnx/K+g5ra2ar3iLoWFQ8vf/EvpuDF1LigIT6tDjRKdA=="], - "@onkernel/sdk": ["@onkernel/sdk@0.109.0", "", {}, "sha512-G8Rxz4bC4fiar1GqoJWeXJeHmXJxDJHnh4LsPZdT8Tf8I9752ZM8CB8FmvvWoGzKlTRTeXlzgKYliH1bW/d93Q=="], + "@onkernel/sdk": ["@onkernel/sdk@github:kernel/kernel-node-sdk#555797f", {}, "kernel-kernel-node-sdk-555797f", "sha512-ZTebOkfkqk9ZvFTytCNW+2QwHqDhPFP6+RlgYFz6zp6Cr83nbgl+inpa2RSSaFSpIXpoMs0Mdv4IRyhSXgufbg=="], "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="], diff --git a/package.json b/package.json index 184da1a..ee5b3bc 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@clerk/themes": "^2.4.19", "@modelcontextprotocol/sdk": "1.26.0", "@onkernel/managed-auth-react": "0.5.3", - "@onkernel/sdk": "^0.109.0", + "@onkernel/sdk": "github:kernel/kernel-node-sdk#555797f46b7b77f2d7c9f45fe663a4f7e8ffe981", "@posthog/mcp": "0.10.1", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", diff --git a/src/lib/mcp/tools/vault-credential-flow.test.ts b/src/lib/mcp/tools/vault-credential-flow.test.ts index fd5ed75..3b1d789 100644 --- a/src/lib/mcp/tools/vault-credential-flow.test.ts +++ b/src/lib/mcp/tools/vault-credential-flow.test.ts @@ -8,6 +8,7 @@ const spec = { fields: [ { name: "username", + label: "Membership Number or Username", type: "text", required: true, sensitive: false, @@ -168,6 +169,7 @@ describe("MCP credential flow", () => { ); const items = tools.find((tool) => tool.name === "manage_vault_items"); expect(credentials?.inputSchema.properties).toHaveProperty("spec"); + expect(JSON.stringify(credentials?.inputSchema)).toContain('"label"'); expect(credentials?.inputSchema.properties).toHaveProperty( "expected_item_id", ); @@ -333,6 +335,9 @@ describe("MCP credential flow", () => { ); expect(created.item.action.url).toBe(pending.action.url); expect(created.item.spec.fields).toEqual(spec.fields); + expect(created.item.spec.fields[0].label).toBe( + "Membership Number or Username", + ); expect( ( await fixture.call("manage_vault_items", { @@ -496,6 +501,17 @@ describe("MCP credential flow", () => { fields: [{ name: "password", type: "password", sensitive: false }], }, }, + ...[ + "", + " Username", + "Username ", + "User\nname", + "User\u200bname", + "x".repeat(129), + ].map((label) => ({ + action: "create", + spec: { fields: [{ name: "username", label, type: "text" }] }, + })), { action: "create", spec: { diff --git a/src/lib/mcp/tools/vault-credentials.ts b/src/lib/mcp/tools/vault-credentials.ts index 32e3cc1..3d4ac2c 100644 --- a/src/lib/mcp/tools/vault-credentials.ts +++ b/src/lib/mcp/tools/vault-credentials.ts @@ -18,11 +18,23 @@ import { const text = () => z.string().refine((value) => Buffer.byteLength(value, "utf8") <= 16384); const fieldName = () => z.string().regex(/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/); +const fieldLabel = () => + z + .string() + .min(1) + .refine((label) => label === label.trim()) + .refine((label) => Buffer.byteLength(label, "utf8") <= 128) + .refine((label) => !/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(label)); const definition = z .object({ name: fieldName().describe( "Stable field name used for updates and browser fills.", ), + label: fieldLabel() + .optional() + .describe( + "Optional non-secret display text for users. The form falls back to name. Labels never affect updates or browser fills.", + ), type: z.enum(["text", "email", "password", "totp"]), required: z.boolean().optional(), sensitive: z @@ -101,7 +113,7 @@ export function registerVaultCredentialTools( ) { server.tool( "manage_vault_credentials", - 'Create or update credential items in a per-end-user vault. Use only the recognizable site name as description; explicitly set sensitive:false for ordinary usernames/emails. Passwords and TOTP seeds must be sensitive. Never store payment-card data here. For human collection, omit values and present the returned bearer collection URL privately to the intended user, outside the agent-controlled browser. Never ask for passwords or TOTP seeds in chat. TOTP seeds require trusted provisioning and have no hosted input. On create, fields is an ordered array of named definitions: inspect the website and list fields in its natural top-to-bottom order because this directly controls the user-facing collection form. Update fields remain keyed by name and contain only value. Updates require the latest version and optionally expected_item_id from an earlier read; definitions are immutable. Omitted values are preserved; null or empty strings clear supported values. Clearing required TOTP is unsupported. Hosted forms require populated required inputs. To reopen collection, use manage_vault_items with action: "invoke" and operation: "collect". Use manage_vault_items get with wait for readiness, then invoke fill with fill parameters. For edits to already-ready items compare versions without wait. Explicitly non-sensitive text/email values are returned; sensitive values and TOTP seeds are omitted. Writes are never automatically retried; reconcile conflicts or uncertain outcomes before any further write.', + 'Create or update credential items in a per-end-user vault. Use only the recognizable site name as description; explicitly set sensitive:false for ordinary usernames/emails. Each field may include an optional non-secret human-readable label; name remains the stable key for updates and browser fills. Passwords and TOTP seeds must be sensitive. Never store payment-card data here. For human collection, omit values and present the returned bearer collection URL privately to the intended user, outside the agent-controlled browser. Never ask for passwords or TOTP seeds in chat. TOTP seeds require trusted provisioning and have no hosted input. On create, fields is an ordered array of named definitions: inspect the website and list fields in its natural top-to-bottom order because this directly controls the user-facing collection form. Update fields remain keyed by name and contain only value. Updates require the latest version and optionally expected_item_id from an earlier read; definitions are immutable. Omitted values are preserved; null or empty strings clear supported values. Clearing required TOTP is unsupported. Hosted forms require populated required inputs. To reopen collection, use manage_vault_items with action: "invoke" and operation: "collect". Use manage_vault_items get with wait for readiness, then invoke fill with fill parameters. For edits to already-ready items compare versions without wait. Explicitly non-sensitive text/email values are returned; sensitive values and TOTP seeds are omitted. Writes are never automatically retried; reconcile conflicts or uncertain outcomes before any further write.', vaultToolInput({ ...vaultItemSchema, key: vaultKeySchema(), diff --git a/src/lib/mcp/vault-responses.ts b/src/lib/mcp/vault-responses.ts index 32477eb..a26a3c6 100644 --- a/src/lib/mcp/vault-responses.ts +++ b/src/lib/mcp/vault-responses.ts @@ -38,7 +38,7 @@ export const vaultItemFields: OutputFields = { ...fields( "provider wallet user_id payment_method_id card_id amount currency merchant merchant_name merchant_url context expires_at description", ), - fields: fields("name type required sensitive"), + fields: fields("name label type required sensitive"), provider_config: fields("id name"), authorization: { method: null, @@ -129,6 +129,7 @@ const credentialValuesSchema = z fields: z.array( z.object({ name: z.string(), + label: z.string().optional(), type: z.string(), sensitive: z.boolean().optional(), }), @@ -332,7 +333,7 @@ export function vaultItemResponse( "Present the collection URL only to the intended user in a private surface, outside the agent-controlled browser. It is a bearer credential. Never ask for passwords or TOTP seeds in chat; TOTP seeds require trusted backend provisioning, not hosted collection.", "MCP returns field definitions, has_value, version, collection expiry, and explicitly non-sensitive text/email values. Sensitive values and TOTP seeds are never returned. Ready means required values exist, not that login succeeded. Listing does not renew collection links; use get or the advertised collect operation.", 'Use manage_vault_items with action: "invoke" and operation: "collect" to reopen the full form without clearing values or changing readiness or version. wait observes readiness, not edits to ready items. Compare versions with get without wait; a change can also come from an API update, so it does not identify a specific form submission.', - "Create or update credentials with manage_vault_credentials. On create, inspect the website and list the named field definitions in its natural top-to-bottom order; that array order directly controls the user-facing collection form. Use a per-user vault, a recognizable site-name-only description, and sensitive:false for usernames/emails. Passwords and TOTP must be sensitive. Updates require the current version; supply expected_item_id when bound to an earlier read. Omitted values remain; null or empty strings clear supported fields, including required text/email/password fields. Hosted forms still require populated required inputs. Do not store payment-card data in credential items.", + "Create or update credentials with manage_vault_credentials. On create, inspect the website and list the named field definitions in its natural top-to-bottom order; that array order directly controls the user-facing collection form. Use optional non-secret labels for human-readable text; stable names remain authoritative for state, updates, and fill. Use a per-user vault, a recognizable site-name-only description, and sensitive:false for usernames/emails. Passwords and TOTP must be sensitive. Updates require the current version; supply expected_item_id when bound to an earlier read. Omitted values remain; null or empty strings clear supported fields, including required text/email/password fields. Hosted forms still require populated required inputs. Do not store payment-card data in credential items.", "Invocation hints are not approval to execute. Invoke fill with manage_vault_items using a fill object containing browser_id and ordered fields of field/selector bindings, never values. Bind the vault at browser creation, authorize the destination, and follow the advertised description. Fill does not submit or navigate; real values enter the browser and may be read by an agent with browser access. Never retry an uncertain fill or fall back to aliases.", ] : [ diff --git a/src/lib/mcp/vault-steering.test.ts b/src/lib/mcp/vault-steering.test.ts index 3427482..934bec4 100644 --- a/src/lib/mcp/vault-steering.test.ts +++ b/src/lib/mcp/vault-steering.test.ts @@ -14,6 +14,7 @@ const credential = { fields: [ { name: "username", + label: "Membership Number or Username", type: "text", required: true, sensitive: false, @@ -162,6 +163,7 @@ describe("vault OpenAPI steering", () => { expect(result.item.spec.description).toBe("Hacker News"); expect(result.item.spec.fields[0]).toEqual({ name: "username", + label: "Membership Number or Username", type: "text", required: true, sensitive: false, @@ -287,7 +289,12 @@ describe("vault OpenAPI steering", () => { spec: { ...credential.spec, fields: [ - { name: "password", type: "password", value: "private-value" }, + { + name: "password", + label: "Account Password", + type: "password", + value: "private-value", + }, { name: "username", type: "text", sensitive: false }, ], }, @@ -296,7 +303,7 @@ describe("vault OpenAPI steering", () => { ), ); expect(result.item.spec.fields).toEqual([ - { name: "password", type: "password" }, + { name: "password", label: "Account Password", type: "password" }, { name: "username", type: "text", sensitive: false }, ]); expect(JSON.stringify(result)).not.toContain("private-value"); From 4acead4b22d3a8eb4e30c1fe1f6515a133fe38f1 Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Fri, 18 Sep 2026 18:12:08 +0000 Subject: [PATCH 2/4] Keep label parsing out of value projection --- src/lib/mcp/vault-responses.ts | 1 - src/lib/mcp/vault-steering.test.ts | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/mcp/vault-responses.ts b/src/lib/mcp/vault-responses.ts index a26a3c6..cc6509d 100644 --- a/src/lib/mcp/vault-responses.ts +++ b/src/lib/mcp/vault-responses.ts @@ -129,7 +129,6 @@ const credentialValuesSchema = z fields: z.array( z.object({ name: z.string(), - label: z.string().optional(), type: z.string(), sensitive: z.boolean().optional(), }), diff --git a/src/lib/mcp/vault-steering.test.ts b/src/lib/mcp/vault-steering.test.ts index 934bec4..4622fbc 100644 --- a/src/lib/mcp/vault-steering.test.ts +++ b/src/lib/mcp/vault-steering.test.ts @@ -64,7 +64,12 @@ describe("vault OpenAPI steering", () => { ...credential, spec: { fields: [ - { name: "username", type: "text", sensitive: false }, + { + name: "username", + label: null, + type: "text", + sensitive: false, + }, { name: "password", type: "password" }, ], }, From c04f44a6b91566b426a82bb4b6ef7ea93c2248a7 Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Fri, 18 Sep 2026 19:25:30 +0000 Subject: [PATCH 3/4] Document credential label constraints --- src/lib/mcp/tools/vault-credential-flow.test.ts | 3 +++ src/lib/mcp/tools/vault-credentials.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/mcp/tools/vault-credential-flow.test.ts b/src/lib/mcp/tools/vault-credential-flow.test.ts index 3b1d789..301f24d 100644 --- a/src/lib/mcp/tools/vault-credential-flow.test.ts +++ b/src/lib/mcp/tools/vault-credential-flow.test.ts @@ -170,6 +170,9 @@ describe("MCP credential flow", () => { const items = tools.find((tool) => tool.name === "manage_vault_items"); expect(credentials?.inputSchema.properties).toHaveProperty("spec"); expect(JSON.stringify(credentials?.inputSchema)).toContain('"label"'); + expect(JSON.stringify(credentials?.inputSchema)).toContain( + "128 UTF-8 bytes", + ); expect(credentials?.inputSchema.properties).toHaveProperty( "expected_item_id", ); diff --git a/src/lib/mcp/tools/vault-credentials.ts b/src/lib/mcp/tools/vault-credentials.ts index 3d4ac2c..9ece44d 100644 --- a/src/lib/mcp/tools/vault-credentials.ts +++ b/src/lib/mcp/tools/vault-credentials.ts @@ -33,7 +33,7 @@ const definition = z label: fieldLabel() .optional() .describe( - "Optional non-secret display text for users. The form falls back to name. Labels never affect updates or browser fills.", + "Optional non-secret display text for users. Must be nonempty, have no leading or trailing whitespace, be at most 128 UTF-8 bytes, and contain no control, formatting, or line-separator characters. The form falls back to name. Labels never affect updates or browser fills.", ), type: z.enum(["text", "email", "password", "totp"]), required: z.boolean().optional(), From 8b3520ecbc65cc60b6c827ceb0a036b80481e9b7 Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Fri, 18 Sep 2026 20:20:46 +0000 Subject: [PATCH 4/4] Use released SDK for credential labels --- bun.lock | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 05af7c4..b6088b1 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,7 @@ "@clerk/themes": "^2.4.19", "@modelcontextprotocol/sdk": "1.26.0", "@onkernel/managed-auth-react": "0.5.3", - "@onkernel/sdk": "github:kernel/kernel-node-sdk#555797f46b7b77f2d7c9f45fe663a4f7e8ffe981", + "@onkernel/sdk": "^0.110.0", "@posthog/mcp": "0.10.1", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11", @@ -154,7 +154,7 @@ "@onkernel/managed-auth-react": ["@onkernel/managed-auth-react@0.5.3", "", { "dependencies": { "clsx": "^2.1.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5Ps7h7HknAxL4ZLnJCzsLhW8MLRr0SH1P8+eALBZXCmnx/K+g5ra2ar3iLoWFQ8vf/EvpuDF1LigIT6tDjRKdA=="], - "@onkernel/sdk": ["@onkernel/sdk@github:kernel/kernel-node-sdk#555797f", {}, "kernel-kernel-node-sdk-555797f", "sha512-ZTebOkfkqk9ZvFTytCNW+2QwHqDhPFP6+RlgYFz6zp6Cr83nbgl+inpa2RSSaFSpIXpoMs0Mdv4IRyhSXgufbg=="], + "@onkernel/sdk": ["@onkernel/sdk@0.110.0", "", {}, "sha512-lUMXEcp8FsQkcVV8AzGU+dkrA9f5ucaaYMIDA6Y2N9oHCtcBENiUKFJpxZTA/+Oc9by8Zj+3H10qajCsWUgDiw=="], "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="], diff --git a/package.json b/package.json index ee5b3bc..aef4a69 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ "@clerk/themes": "^2.4.19", "@modelcontextprotocol/sdk": "1.26.0", "@onkernel/managed-auth-react": "0.5.3", - "@onkernel/sdk": "github:kernel/kernel-node-sdk#555797f46b7b77f2d7c9f45fe663a4f7e8ffe981", + "@onkernel/sdk": "^0.110.0", "@posthog/mcp": "0.10.1", "@types/jsonwebtoken": "^9.0.10", "@types/redis": "^4.0.11",