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
21 changes: 21 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: CI

on:
push:
pull_request:

permissions:
contents: read

jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.4.3
- run: bun install --frozen-lockfile
- run: bun run check
- run: bun run test
- run: bun run build
116 changes: 64 additions & 52 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A minimal MCP (Model Context Protocol) server library for Bun with auth forwardi
bunx create-futurity-plugin@latest my-plugin
cd my-plugin
cp .env.example .env
bun run keygen
bun run keygen # copy the private key into FUTURITY_SIGNING_KEY in .env
bun run dev
```

Expand All @@ -25,7 +25,21 @@ bun add @futurity/plugins
```ts
import { mcp, t } from "@futurity/plugins";

const signingKey = process.env.FUTURITY_SIGNING_KEY;
if (!signingKey) throw new Error("Set FUTURITY_SIGNING_KEY to your Ed25519 private key.");

const pluginManifest = {
pluginId: "my-plugin",
developerEmail: "developer@example.com",
name: "My Plugin",
version: "1.0.0",
mcpUrl: `${process.env.PUBLIC_URL ?? "http://localhost:3000"}/mcp`,
auth: { type: "none" as const },
signingKey,
};

const app = mcp({
pluginManifest,
name: "my-plugin",
version: "1.0.0",
});
Expand All @@ -51,7 +65,13 @@ await app.listen(3000);

### `mcp(options)`

Create an MCP application.
Create an MCP application. `pluginManifest` is required, including for WebSocket
servers; omitting it throws at construction. The manifest is validated and signed
when `app.fetch` is first accessed or a transport starts. The SDK supplies
`specVersion` automatically; you maintain only your plugin's release `version`.

The snippets below reuse `pluginManifest` from Basic usage; configure its identity,
auth, and public MCP URL for your own plugin.

```typescript
const app = mcp({
Expand All @@ -60,7 +80,7 @@ const app = mcp({
path: "/mcp", // default: "/mcp"
instructions: "...", // optional system instructions
capabilities: { ... }, // optional MCP capabilities
pluginManifest: { ... }, // optional auth forwarding manifest
pluginManifest, // required signed v4 manifest
});
```

Expand Down Expand Up @@ -156,7 +176,7 @@ await app.listen(3000, "websocket");
```typescript
import { mcp, cors } from "@futurity/plugins";

const app = mcp({ name: "server", version: "1.0.0" });
const app = mcp({ name: "server", version: "1.0.0", pluginManifest });

app.use(
cors({
Expand All @@ -180,6 +200,7 @@ Configure OAuth metadata for `/.well-known/oauth-authorization-server`:
const app = mcp({
name: "server",
version: "1.0.0",
pluginManifest,
oauth: {
issuer: "https://auth.example.com",
authorizationEndpoint: "https://auth.example.com/oauth/authorize",
Expand All @@ -192,12 +213,12 @@ const app = mcp({

## Authentication

There are two auth modes for Futurity plugins:
SDK v3 (`@futurity/plugins@^3.0.0`) emits custom-plugin manifests using `specVersion: 4` only. See [Manifest v4](docs/manifest-v4.md)
for migration, signed metadata, organization credentials, and governance.

| Mode | Description | Use Case |
|------|-------------|----------|
| **Auth Forwarding** (v1) | Platform manages OAuth tokens | Simple integrations |
| **Chained Auth** (v2) | Plugin manages sessions & tokens | Complex integrations, multi-service |
Custom registration supports `forwarding` (platform-managed OAuth) and `none`
(no OAuth exchange, optionally with explicit organization credentials).
Chained authentication is not supported by custom registration.

### Auth Forwarding (v1)

Expand All @@ -209,7 +230,8 @@ Auth forwarding lets the Futurity platform manage OAuth tokens on behalf of your
bun run keygen
```

This prints an Ed25519 keypair. Keep the private key secret; register the public key with the Futurity API.
In a generated project, this prints an Ed25519 keypair. Set `FUTURITY_SIGNING_KEY`
to the private key before starting the server. Keep the private key secret; register the public key with the Futurity API.

#### 2. Configure the plugin manifest

Expand All @@ -218,13 +240,14 @@ const app = mcp({
name: "my-plugin",
version: "1.0.0",
pluginManifest: {
specVersion: 2,
pluginId: "my-plugin",
developerEmail: "developer@example.com",
name: "My Plugin",
version: "1.0.0",
signingKey: process.env.FUTURITY_SIGNING_KEY!,
signingKey, // required key loaded above
auth: {
type: "forwarding",
grantType: "authorization_code",
tokenEndpoint: "https://auth.example.com/oauth2/token",
authorizationEndpoint: "https://auth.example.com/oauth2/authorize",
requiredScopes: ["read", "write"],
Expand All @@ -246,47 +269,15 @@ GET /.well-known/futurity/plugin

The response includes an `X-Futurity-Signature` header with an Ed25519 JWS signature.

### Chained Auth (v2)

For plugins that need to manage their own sessions and store third-party tokens, use chained auth. The plugin controls the OAuth flow and issues its own tokens.
### Standalone chained-auth runtime

```typescript
const app = mcp({
name: "my-plugin",
version: "1.0.0",
pluginManifest: {
specVersion: 2,
pluginId: "my-plugin",
name: "My Plugin",
version: "1.0.0",
signingKey: process.env.PLUGIN_SIGNING_KEY!,
auth: {
type: "chained",
authorizationEndpoint: "https://plugin.example.com/auth/authorize",
callbackEndpoint: "https://plugin.example.com/auth/callback",
tokenEndpoint: "https://plugin.example.com/auth/token",
requiredUserContext: ["user_id", "email"],
},
mcpUrl: "https://plugin.example.com/mcp",
},
chainedAuth: {
sessionStore: mySessionStore,
platformJwksUrl: "https://platform.example.com/.well-known/jwks.json",
pluginSigningKey: process.env.PLUGIN_SIGNING_KEY!,
handlers: {
onAuthorize: async (userContext, platformState, platformCallback) => { ... },
onCallback: async (req) => { ... },
onToken: async (req) => { ... },
},
},
});
```
The session, token, and `chainedAuth` routing utilities remain available for
separately integrated hosts. They are not a v4 manifest authentication option;
`PluginManifestSchema` rejects `auth.type: "chained"`. Do not declare `none` as a
workaround for a chained plugin.

See [docs/chained-auth.md](docs/chained-auth.md) for full documentation including:
- Complete implementation guide
- Request binding for anti-replay protection
- Session management
- Platform integration guide
See [docs/chained-auth.md](docs/chained-auth.md) for the runtime protocol and
host integration requirements.

### Signing utilities

Expand All @@ -306,6 +297,7 @@ Custom auth middleware:
const app = mcp({
name: "server",
version: "1.0.0",
pluginManifest,
auth: async (req) => {
const token = req.headers.get("authorization")?.replace("Bearer ", "");
if (!token) return false;
Expand Down Expand Up @@ -343,7 +335,7 @@ app.tool("set", {
The HTTP transport supports multiple concurrent client sessions.

```typescript
const app = mcp({ name: "server", version: "1.0.0" });
const app = mcp({ name: "server", version: "1.0.0", pluginManifest });

app.tool("example", { handler: async () => ({ ok: true }) });

Expand Down Expand Up @@ -378,6 +370,9 @@ The `examples/` directory in this repo contains runnable examples. Clone the rep
```bash
git clone https://github.com/futuritywork/plugins.git
cd plugins
# Generate a keypair; keep the private key secret.
bun -e 'import { generateKeyPair } from "./packages/plugins/src/signing"; console.log(generateKeyPair());'
export FUTURITY_SIGNING_KEY="<privateKey from the output>"
bun examples/calculator.ts # Math operations and unit conversion
bun examples/cors.ts # CORS configuration
bun examples/database.ts # Document database
Expand All @@ -389,6 +384,23 @@ bun examples/weather-api.ts # Weather data API
bun examples/monday/index.ts # monday.com integration with auth forwarding
```

Each example requires `FUTURITY_SIGNING_KEY`. The local examples publish
`http://localhost:3000/mcp` by default; set `PUBLIC_URL` to the public HTTPS base
URL before registration. They use `auth.type: "none"` because their tools do not
require OAuth; `oauth.ts` demonstrates discovery metadata only.

### Repository verification

```bash
bun install --frozen-lockfile
bun run check
bun run test
bun run build
```

`check` typechecks the SDK, all examples, and the scaffold template's TypeScript
against the local SDK source, without requiring a prior build.

## monday.com Integration Example

A complete monday.com MCP server is included demonstrating auth forwarding:
Expand Down
Loading
Loading