Skip to content

Quick Start

lynq is a framework for building MCP servers with middleware and session-scoped tool visibility.

Install

sh
pnpm add @lynq/lynq zod
sh
npm install @lynq/lynq zod
sh
yarn add @lynq/lynq zod
sh
bun add @lynq/lynq zod

Minimal Server

ts
// server.ts
import { createMCPServer } from "@lynq/lynq";
import { z } from "zod";

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

server.tool(
  "greet",
  {
    description: "Say hello",
    input: z.object({ name: z.string() }),
  },
  async (args, c) => c.text(`Hello, ${args.name}!`),
);

await server.stdio();

Run it:

sh
npx tsx server.ts

That's it. One tool, stdio transport, zero config.

Connect to Claude Code

Add to your Claude Code config (claude_code_config.json):

json
{
  "mcpServers": {
    "my-server": {
      "command": "npx",
      "args": ["tsx", "server.ts"]
    }
  }
}

Restart Claude Code. The greet tool appears in the tool list.

Under the hood

server.stdio() creates an StdioServerTransport from the MCP SDK and connects it to the internal server. The client spawns your process, then communicates over stdin/stdout using JSON-RPC. All MCP protocol negotiation (capabilities, initialization) is handled by the SDK.

Add Auth-Protected Tools

lynq's core feature is session-scoped tool visibility. Tools guarded by middleware are hidden until the session is authorized. Here's an example using the built-in guard() middleware:

ts
import { createMCPServer } from "@lynq/lynq";
import { guard } from "@lynq/lynq/guard";
import { z } from "zod";

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

// Always visible -- the entry point
server.tool(
  "login",
  {
    description: "Login with credentials",
    input: z.object({
      username: z.string(),
      password: z.string(),
    }),
  },
  async (args, c) => {
    if (args.username === "admin" && args.password === "1234") {
      c.session.set("user", { name: args.username });
      c.session.authorize("guard");
      return c.text(`Welcome, ${args.username}.`);
    }
    return c.error("Invalid credentials.");
  },
);

// Hidden until c.session.authorize("guard") is called
server.tool(
  "get_weather",
  guard(),
  {
    description: "Get current weather for a city",
    input: z.object({ city: z.string() }),
  },
  async (args, c) => c.text(`${args.city}: 22C, Sunny`),
);

await server.stdio();

guard() is a middleware that demonstrates the visibility pattern. For production use cases, write your own middleware tailored to your auth system -- see Custom Middleware.

Verify Visibility

Before login, the client sees:

ToolVisible
loginYes
get_weatherNo

After calling login with valid credentials:

ToolVisible
loginYes
get_weatherYes

The client receives the notification automatically. No manual sendToolListChanged call needed.

Next Steps