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 .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
dist
*.tsbuildinfo
*.tsbuildinfo
.env
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit",
"source.fixAll.biome": "explicit"
Comment on lines +5 to +6

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need to manually set those 2 ?

},
"biome.lsp.bin": "${workspaceFolder}/node_modules/.bin/biome"
}
14 changes: 14 additions & 0 deletions examples/mixed-auth/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# WorkOS AuthKit Configuration
# Get these from https://dashboard.workos.com

# Your AuthKit domain (e.g., "yourapp.authkit.app")
AUTHKIT_DOMAIN=

# Your WorkOS API key (starts with "sk_")
WORKOS_API_KEY=

# Server URL (for development)
SERVER_URL=http://localhost:3000

# Environment
NODE_ENV=development
5 changes: 5 additions & 0 deletions examples/mixed-auth/nodemon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"watch": ["server/src"],
"ext": "ts,json",
"exec": "tsx server/src/index.ts"
}
46 changes: 46 additions & 0 deletions examples/mixed-auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "skybridge-mixed-auth-example",
"version": "0.0.1",
"private": true,
"description": "Skybridge Mixed Auth Example - Coffee Shop Finder",
"type": "module",
"scripts": {
"dev": "skybridge",
"build": "skybridge build",
"start": "skybridge start",
"inspector": "mcp-inspector http://localhost:3000/mcp",
"server:build": "tsc -p tsconfig.server.json",
"server:start": "node dist/index.js",
"web:build": "tsc -b web && vite build -c web/vite.config.ts",
"web:preview": "vite preview -c web/vite.config.ts"
Comment on lines +12 to +15

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those are legacy, you can remove

},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",
"@workos-inc/node": "^7.72.2",
"dotenv": "^16.5.0",
"express": "^5.2.1",
"jose": "^6.0.11",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"skybridge": ">=0.22.0 <1.0.0",
"vite": "^7.3.0",
"zod": "^4.3.5"
},
"devDependencies": {
"@modelcontextprotocol/inspector": "^0.18.0",
"@skybridge/devtools": ">=0.22.0 <1.0.0",
"@types/express": "^5.0.6",
"@types/node": "^22.15.30",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"nodemon": "^3.1.11",
"shx":"^0.4.0",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
},
"workspaces": [],
"engines": {
"node": ">=24.0.0"
}
}
99 changes: 99 additions & 0 deletions examples/mixed-auth/server/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
import type {
ServerNotification,
ServerRequest,
} from "@modelcontextprotocol/sdk/types.js";
import { WorkOS } from "@workos-inc/node";
import * as jose from "jose";
import { env } from "./env.js";

type Extra = RequestHandlerExtra<ServerRequest, ServerNotification>;

// Initialize JWKS client for AuthKit public key verification
const jwks = jose.createRemoteJWKSet(
new URL(`https://${env.AUTHKIT_DOMAIN}/oauth2/jwks`),
);

// Initialize WorkOS client
const workos = new WorkOS(env.WORKOS_API_KEY);

interface AuthResult {
userId: string;
email: string;
firstName: string | null;
lastName: string | null;
}

export async function tryGetAuth(
extra: Extra,
): Promise<AuthResult | undefined> {
const authHeader = extra.requestInfo?.headers?.authorization;
if (!authHeader) {
return undefined;
}

const headerValue = Array.isArray(authHeader) ? authHeader[0] : authHeader;
if (!headerValue?.toLowerCase().startsWith("bearer ")) {
return undefined;
}

const token = headerValue.slice(7).trim();
if (!token) {
return undefined;
}

try {
const { payload } = await jose.jwtVerify(token, jwks, {
issuer: `https://${env.AUTHKIT_DOMAIN}`,
});

if (!payload.sub || typeof payload.sub !== "string") {
return undefined;
}

const user = await workos.userManagement.getUser(payload.sub);

return {
userId: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
};
} catch {
return undefined;
}
}

export async function isAuthenticated(extra: Extra): Promise<boolean> {
const auth = await tryGetAuth(extra);
return auth !== undefined;
}

export async function getUserId(extra: Extra): Promise<string | undefined> {
const auth = await tryGetAuth(extra);
return auth?.userId;
}

export function buildWwwAuthenticateHeader(
resourceMetadataUrl: string,
scopes?: string[],
): string {
let header = `Bearer resource_metadata="${resourceMetadataUrl}"`;
if (scopes && scopes.length > 0) {
header += `, scope="${scopes.join(" ")}"`;
}
return header;
}

export function oauthErrorResult(
wwwAuthenticateValue: string,
message: string,
) {
return {
content: [{ type: "text" as const, text: message }],
isError: true,
_meta: {
"mcp/www_authenticate": [wwwAuthenticateValue],
},
};
}
Loading