Skip to content

credentials()

Form-based authentication via elicitation. Tools are hidden until the user submits valid credentials.

Import

ts
import { credentials } from "@lynq/lynq/credentials";

Usage

ts
import { z } from "zod";

server.tool(
  "dashboard",
  credentials({
    message: "Login required",
    schema: z.object({ username: z.string(), password: z.string() }),
    verify: async (fields) => {
      const user = await db.findUser(fields.username, fields.password);
      return user ?? null;
    },
  }),
  config,
  handler,
);

Options

OptionTypeDefaultDescription
namestring"credentials"Middleware name for authorize() / revoke()
messagestring(required)Message shown in the elicitation form
schemaz.ZodObject(required)Zod schema defining form fields
verify(fields) => Promise<unknown | null>(required)Return user data or null to reject
sessionKeystring"user"Session key to store verified user

Peer dependency

credentials() requires zod as a peer dependency. Install it alongside lynq:

sh
pnpm add zod
sh
npm install zod
sh
yarn add zod
sh
bun add zod

:::

Example

ts
import { createMCPServer, text } from "@lynq/lynq";
import { credentials } from "@lynq/lynq/credentials";
import { z } from "zod";

const server = createMCPServer({ name: "my-app", version: "1.0.0" });

const loginForm = credentials({
  message: "Please enter your credentials to continue.",
  schema: z.object({
    username: z.string().min(1),
    password: z.string().min(8),
  }),
  verify: async (fields) => {
    const user = await db.authenticate(fields.username, fields.password);
    return user ?? null; // null rejects with "Invalid credentials."
  },
});

server.tool(
  "account-settings",
  loginForm,
  { description: "View and edit account settings" },
  async (c) => {
    const user = c.session.get("user");
    return text(`Logged in as ${user.name}. Email: ${user.email}`);
  },
);

server.tool(
  "billing",
  loginForm,
  { description: "View billing history" },
  async (c) => {
    const user = c.session.get("user");
    const invoices = await db.getInvoices(user.id);
    return text(JSON.stringify(invoices));
  },
);

Both tools are hidden until the user authenticates. The first call to either tool triggers the elicitation form. Once verified, both tools become visible and subsequent calls skip the form.

Under the hood

onRegister() returns false to hide tools from the initial tools/list. On the first call, the onCall hook checks session.get(sessionKey). If absent, it calls c.elicit.form(message, schema) to prompt the user for input. If the user accepts and verify() returns a non-null value, the result is stored in the session and authorize(name) is called, which triggers notifications/tools/list_changed. If the user cancels or verify() returns null, an error is returned and the tool remains hidden.