Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This is a pnpm monorepo with the following packages:

- `packages/oauth` (`@keycardai/oauth`) - Pure OAuth 2.0 primitives (no MCP dependency)
- `packages/mcp` (`@keycardai/mcp`) - MCP-specific OAuth integration
- `packages/pi-mono` (`@keycardai/pi-mono`) - Pi-mono agent integration (MCP tool → AgentTool conversion, OAuth auth)
- `packages/sdk` (`@keycardai/sdk`) - Aggregate package re-exporting from oauth + mcp

## Git Commits
Expand All @@ -14,7 +15,7 @@ Follow conventional commits: `type(scope): description`

Types: `docs`, `feat`, `fix`, `refactor`, `test`, `chore`

Scopes: `oauth`, `mcp`, `sdk`, `deps`, `docs`
Scopes: `oauth`, `mcp`, `pi-mono`, `sdk`, `deps`, `docs`

## Build Order

Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@ A collection of TypeScript packages for Keycard services, organized as a pnpm wo
|---|---|---|
| [`@keycardai/oauth`](packages/oauth/) | Pure OAuth 2.0 primitives — JWKS key management, JWT signing/verification, authorization server discovery | [![npm](https://img.shields.io/npm/v/@keycardai/oauth)](https://www.npmjs.com/package/@keycardai/oauth) |
| [`@keycardai/mcp`](packages/mcp/) | MCP OAuth integration — Express middleware, token verification, client providers | [![npm](https://img.shields.io/npm/v/@keycardai/mcp)](https://www.npmjs.com/package/@keycardai/mcp) |
| [`@keycardai/pi-mono`](packages/pi-mono/) | Pi-mono agent integration — MCP tool conversion, OAuth auth, system prompts | [![npm](https://img.shields.io/npm/v/@keycardai/pi-mono)](https://www.npmjs.com/package/@keycardai/pi-mono) |
| [`@keycardai/sdk`](packages/sdk/) | Aggregate package re-exporting from oauth + mcp | [![npm](https://img.shields.io/npm/v/@keycardai/sdk)](https://www.npmjs.com/package/@keycardai/sdk) |

## Which Package?

| You want to... | Install |
|---|---|
| Add auth to your MCP server (Express) | `@keycardai/mcp` |
| Connect to MCP servers from a pi-mono agent | `@keycardai/pi-mono` |
| Use OAuth 2.0 primitives directly | `@keycardai/oauth` |

## Key Concepts

- **Zone** — A Keycard environment that groups your identity providers, MCP resources, and access policies. Get your zone ID from [console.keycard.ai](https://console.keycard.ai).
Expand All @@ -36,6 +45,14 @@ npm install @keycardai/mcp @modelcontextprotocol/sdk
`@keycardai/mcp` requires `@modelcontextprotocol/sdk` as a peer dependency (`^1.15.0`).
This includes `@keycardai/oauth` as a dependency.

### For Pi-Mono Agent Integration

If you're building a pi-mono agent that connects to Keycard-protected MCP servers:

```bash
npm install @keycardai/pi-mono @mariozechner/pi-agent-core @mariozechner/pi-ai @modelcontextprotocol/sdk @sinclair/typebox
```

### For OAuth Functionality Only

If you only need OAuth primitives (JWT signing, JWKS key management, discovery):
Expand Down
114 changes: 114 additions & 0 deletions examples/pi-mono-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Pi-mono Agent with Keycard MCP Tools

A CLI coding agent powered by [pi-mono](https://github.com/badlogic/pi-mono) that can access Keycard-protected MCP servers (GitHub, Slack, Linear, etc.) through OAuth-authenticated tools. Demonstrates `@keycardai/pi-mono` — the pi-mono integration for the Keycard TypeScript SDK.

## What This Example Shows

- Connecting to Keycard-protected MCP servers via `@modelcontextprotocol/sdk`
- Using `PiMonoClient` to convert MCP tools into pi-mono `AgentTool` instances
- Auth-aware system prompt generation (authorized vs unauthorized servers)
- Console-based OAuth flow for unauthorized servers
- Creating a `createAgentSession()` with Keycard tools

## Prerequisites

- **Node.js 18+**
- **Anthropic API key** (or another provider supported by pi-mono)
- **Keycard account** — sign up at [console.keycard.ai](https://console.keycard.ai)
- **Configured zone** with an identity provider (Okta, Auth0, Google, etc.)
- **At least one MCP server** registered as a resource in Keycard Console

## Keycard Console Setup

### 1. Register Your MCP Server as a Resource

If you have a Keycard-protected MCP server running (see the [Cloudflare Worker](../cloudflare-worker/) or [Hello World](../hello-world-server/) examples):

1. Navigate to **Resources** → **Create Resource**
2. Configure:
- **Resource Name:** `GitHub MCP Server`
- **Resource Identifier:** `https://your-mcp-server.example.com`
- **Credential Provider:** Zone Provider
- **Scopes:** `mcp:tools`

### 2. Create Application Credentials

1. Navigate to **Applications** → **Create Application**
2. Note the **Client ID** and **Client Secret** — you'll need these for the OAuth flow

## Install and Run

```bash
cd examples/pi-mono-agent
npm install
```

Set your API key and MCP server URL:

```bash
export ANTHROPIC_API_KEY=sk-ant-...
export GITHUB_MCP_URL=https://your-mcp-server.example.com/mcp
```

Run the agent:

```bash
npm start
```

Or with a custom prompt:

```bash
npm start -- "List my open pull requests on GitHub"
```

## How It Works

```
┌──────────────┐ ┌───────────────┐ ┌──────────────┐
│ pi-mono │────▶│ PiMonoClient │────▶│ MCP Server │
│ Agent │ │ (Keycard SDK) │ │ (Keycard │
│ Session │ │ │ │ protected) │
│ │◀────│ AgentTool[] │◀────│ │
└──────────────┘ └───────────────┘ └──────────────┘
│ │
│ prompt("List my PRs") │
│ ──▶ calls github__list_pulls │
│ ──▶ MCP callTool() ──────────────▶│
│ ◀── result ◀──────────────│
│ ◀── "You have 3 open PRs..." │
```

1. **Connect** — MCP clients connect to Keycard-protected servers
2. **Detect auth** — `PiMonoClient` probes each server; authorized ones get their tools converted
3. **Convert tools** — MCP tools become pi-mono `AgentTool` instances (JSON Schema → TypeBox)
4. **Create session** — `createAgentSession()` with the converted tools + auth tools
5. **Prompt** — The LLM sees the tools, calls them as needed, results flow back through MCP

## Configuration

Edit the `MCP_SERVERS` array in `src/index.ts` to add your servers:

```typescript
const MCP_SERVERS = [
{ name: "github", url: process.env.GITHUB_MCP_URL ?? "https://..." },
{ name: "slack", url: process.env.SLACK_MCP_URL ?? "https://..." },
{ name: "linear", url: process.env.LINEAR_MCP_URL ?? "https://..." },
];
```

## Environment Variables

| Variable | Description |
|---|---|
| `ANTHROPIC_API_KEY` | Anthropic API key (required) |
| `GITHUB_MCP_URL` | URL of your GitHub MCP server |
| `SLACK_MCP_URL` | URL of your Slack MCP server |
| `LINEAR_MCP_URL` | URL of your Linear MCP server |

## Related

- [`@keycardai/pi-mono` docs](https://docs.keycard.ai/sdk/pi-mono/) — full API reference
- [Cloudflare Worker example](../cloudflare-worker/) — build the MCP server this agent connects to
- [Hello World example](../hello-world-server/) — Express-based MCP server
- [pi-mono SDK docs](https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/sdk.md) — pi-mono framework reference
23 changes: 23 additions & 0 deletions examples/pi-mono-agent/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "pi-mono-agent",
"version": "0.1.0",
"private": true,
"description": "Pi-mono coding agent with Keycard-protected MCP tools — GitHub, Slack, and more",
"type": "module",
"scripts": {
"start": "tsx src/index.ts",
"dev": "tsx watch src/index.ts"
},
"dependencies": {
"@keycardai/pi-mono": "^0.1.0",
"@mariozechner/pi-agent-core": ">=0.60.0",
"@mariozechner/pi-ai": ">=0.60.0",
"@mariozechner/pi-coding-agent": ">=0.60.0",
"@modelcontextprotocol/sdk": "^1.15.0",
"@sinclair/typebox": ">=0.30.0"
},
"devDependencies": {
"tsx": "^4.19.0",
"typescript": "^5.8.3"
}
}
144 changes: 144 additions & 0 deletions examples/pi-mono-agent/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* Pi-mono Coding Agent with Keycard MCP Tools
*
* A CLI agent powered by pi-mono that can access Keycard-protected MCP
* servers (GitHub, Slack, Linear, etc.) through OAuth-authenticated tools.
*
* Prerequisites:
* 1. A Keycard zone with an identity provider configured
* 2. One or more MCP servers registered as resources in Keycard Console
* 3. An Anthropic API key (or other supported provider)
*
* See README.md for full setup instructions.
*/

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import {
createAgentSession,
SessionManager,
AuthStorage,
ModelRegistry,
} from "@mariozechner/pi-coding-agent";
import { PiMonoClient, ConsoleAuthToolHandler } from "@keycardai/pi-mono";

// ---------------------------------------------------------------------------
// Configuration — set these via environment variables
// ---------------------------------------------------------------------------

const MCP_SERVERS = [
{
name: "github",
url: process.env.GITHUB_MCP_URL ?? "https://github-mcp.example.com/mcp",
},
// Add more servers:
// { name: "slack", url: process.env.SLACK_MCP_URL ?? "https://slack-mcp.example.com/mcp" },
// { name: "linear", url: process.env.LINEAR_MCP_URL ?? "https://linear-mcp.example.com/mcp" },
];

const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (!ANTHROPIC_API_KEY) {
console.error("Set ANTHROPIC_API_KEY to run this agent.");
process.exit(1);
}

// ---------------------------------------------------------------------------
// 1. Connect MCP clients to Keycard-protected servers
// ---------------------------------------------------------------------------

console.log("Connecting to MCP servers...");

const connectedServers = await Promise.all(
MCP_SERVERS.map(async (server) => {
const client = new Client({ name: "pi-mono-agent", version: "1.0.0" });
try {
const transport = new StreamableHTTPClientTransport(new URL(server.url));
await client.connect(transport);
console.log(` Connected: ${server.name}`);
} catch (error) {
console.warn(` Failed to connect to ${server.name}:`, error instanceof Error ? error.message : error);
}
return { name: server.name, client };
}),
);

// ---------------------------------------------------------------------------
// 2. Create Keycard adapter and detect auth status
// ---------------------------------------------------------------------------

const keycardClient = new PiMonoClient({
servers: connectedServers,
authHandler: new ConsoleAuthToolHandler(),
generateAuthUrl: async (serverName) => {
// In a real app, you'd discover the authorization endpoint via
// RFC 9728 metadata and build a PKCE auth URL. See the playthis-agent
// example for a full implementation.
return `https://your-zone.keycard.cloud/authorize?server=${serverName}`;
},
});

await keycardClient.detectAuthStatus();

const authorized = keycardClient.getAuthorizedServers();
const unauthorized = keycardClient.getUnauthorizedServers();

console.log(`\nAuthorized: ${authorized.length > 0 ? authorized.join(", ") : "(none)"}`);
if (unauthorized.length > 0) {
console.log(`Needs auth: ${unauthorized.join(", ")}`);
}

// ---------------------------------------------------------------------------
// 3. Get pi-mono tools and system prompt
// ---------------------------------------------------------------------------

const mcpTools = await keycardClient.getTools();
const authTools = await keycardClient.getAuthTools();
const systemPromptSection = keycardClient.getSystemPrompt();

console.log(`\nLoaded ${mcpTools.length} MCP tool(s), ${authTools.length} auth tool(s)`);

// ---------------------------------------------------------------------------
// 4. Create pi-mono agent session
// ---------------------------------------------------------------------------

const authStorage = AuthStorage.create();
authStorage.setRuntimeApiKey("anthropic", ANTHROPIC_API_KEY);
const modelRegistry = ModelRegistry.create(authStorage);

const { session } = await createAgentSession({
sessionManager: SessionManager.inMemory(),
authStorage,
modelRegistry,
tools: [...mcpTools, ...authTools],
systemPrompt: `You are a helpful coding assistant.\n\n${systemPromptSection}`,
});

// ---------------------------------------------------------------------------
// 5. Run a prompt and stream the response
// ---------------------------------------------------------------------------

console.log("\n--- Agent Response ---\n");

session.subscribe((event: Record<string, unknown>) => {
if (
event.type === "message_update" &&
typeof event.assistantMessageEvent === "object" &&
event.assistantMessageEvent !== null
) {
const ame = event.assistantMessageEvent as Record<string, unknown>;
if (ame.type === "text_delta" && typeof ame.delta === "string") {
process.stdout.write(ame.delta);
}
}
});

// Use the first argument as the prompt, or a default
const prompt = process.argv[2] ?? "What tools do you have available? List them briefly.";
await session.prompt(prompt);

console.log("\n");

// Cleanup
for (const server of connectedServers) {
await server.client.close().catch(() => {});
}
12 changes: 12 additions & 0 deletions examples/pi-mono-agent/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2021",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src/**/*"]
}
Loading
Loading