mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-08 03:08:21 +00:00
make mcp oauth only
This commit is contained in:
parent
2c6d5f36ed
commit
2d920c12f0
21 changed files with 49 additions and 221 deletions
17
.github/workflows/claude-auto-fix-ci.yml
vendored
17
.github/workflows/claude-auto-fix-ci.yml
vendored
|
|
@ -77,8 +77,6 @@ jobs:
|
|||
Branch: ${{ github.event.workflow_run.head_branch }}
|
||||
Repository: ${{ github.repository }}
|
||||
|
||||
Check supermemory for similar past CI failures and fixes.
|
||||
|
||||
Fix the CI failures. Common fixes:
|
||||
- Biome lint errors: Run `bun run format-lint` or `biome check --fix .`
|
||||
- Type errors: Run `bun run check-types` and fix reported issues
|
||||
|
|
@ -87,21 +85,8 @@ jobs:
|
|||
After fixing, commit the changes and push directly to the branch `${{ github.event.workflow_run.head_branch }}`.
|
||||
Do NOT create a new PR — the fixes should be pushed to the existing PR branch.
|
||||
|
||||
Save the fix pattern to supermemory for future reference.
|
||||
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
claude_args: |
|
||||
--max-turns 20
|
||||
--model claude-opus-4-5-20251101
|
||||
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,mcp__supermemory,mcp__github"
|
||||
--mcp-config '{
|
||||
"mcpServers": {
|
||||
"supermemory": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.supermemory.ai/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${{ secrets.SUPERMEMORY_API_KEY }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}'
|
||||
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,mcp__github"
|
||||
|
|
|
|||
13
.github/workflows/claude-code-review.yml
vendored
13
.github/workflows/claude-code-review.yml
vendored
|
|
@ -48,18 +48,7 @@ jobs:
|
|||
# Enable inline comments for specific issues
|
||||
claude_args: |
|
||||
--model claude-opus-4-5-20251101
|
||||
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,mcp__supermemory__*,mcp__github__*"
|
||||
--mcp-config '{
|
||||
"mcpServers": {
|
||||
"supermemory": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.supermemory.ai/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${{ secrets.SUPERMEMORY_API_KEY }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}'
|
||||
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,mcp__github__*"
|
||||
|
||||
prompt: |
|
||||
You are a senior engineer reviewing a pull request. Your job is to catch real bugs, security issues, and logic errors that a human reviewer might miss. You are NOT a linter — do not comment on style, naming, formatting, or minor nitpicks.
|
||||
|
|
|
|||
13
.github/workflows/claude.yml
vendored
13
.github/workflows/claude.yml
vendored
|
|
@ -67,15 +67,4 @@ jobs:
|
|||
claude_args: |
|
||||
--max-turns 15
|
||||
--model claude-opus-4-5-20251101
|
||||
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,mcp__supermemory,mcp__github"
|
||||
--mcp-config '{
|
||||
"mcpServers": {
|
||||
"supermemory": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.supermemory.ai/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer ${{ secrets.SUPERMEMORY_API_KEY }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}'
|
||||
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(*),WebSearch,WebFetch,Task,mcp__github"
|
||||
|
|
|
|||
15
README.md
15
README.md
|
|
@ -182,21 +182,6 @@ Add this to your MCP client config:
|
|||
}
|
||||
```
|
||||
|
||||
Or use an API key instead of OAuth:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"supermemory": {
|
||||
"url": "https://mcp.supermemory.ai/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer sm_your_api_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build with Supermemory (API)
|
||||
|
|
|
|||
|
|
@ -158,21 +158,6 @@ MCP 服务器开源——[查看源码](https://supermemory.ai/docs/supermemory-
|
|||
}
|
||||
```
|
||||
|
||||
如果想用 API key 代替 OAuth:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"supermemory": {
|
||||
"url": "https://mcp.supermemory.ai/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer sm_your_api_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 用 Supermemory API 构建
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ When users interact with any connected AI application, the system captures relev
|
|||
### Key Features
|
||||
|
||||
- **OAuth Authentication** - Secure login through Supermemory accounts
|
||||
- **API Key Support** - Alternative authentication for automation and CI/CD
|
||||
- **Persistent Memory** - Save and recall information across sessions
|
||||
- **User Profiles** - Auto-generated profiles from stored memories
|
||||
- **Project Scoping** - Organize memories by project with `x-sm-project` header
|
||||
|
|
@ -36,7 +35,7 @@ When users interact with any connected AI application, the system captures relev
|
|||
|
||||
1. User interacts with any MCP-compatible AI client
|
||||
2. The client connects to `https://mcp.supermemory.ai/mcp`
|
||||
3. OAuth flow authenticates the user (or API key validates directly)
|
||||
3. OAuth flow authenticates the user
|
||||
4. During conversations, relevant information is stored using the `memory` tool
|
||||
5. When context is needed, the `recall` tool retrieves relevant memories
|
||||
6. The AI assistant accesses this persistent context regardless of which platform is being used
|
||||
|
|
@ -45,8 +44,7 @@ When users interact with any connected AI application, the system captures relev
|
|||
|
||||
### Authentication Model
|
||||
|
||||
- **OAuth by default** - Secure authentication through Supermemory accounts
|
||||
- **API key alternative** - Keys start with `sm_` for programmatic access
|
||||
- **OAuth required** - Secure authentication through Supermemory accounts
|
||||
- **Session isolation** - Complete user data separation per account
|
||||
|
||||
### Privacy Features
|
||||
|
|
|
|||
|
|
@ -28,26 +28,7 @@ Add to your MCP client config:
|
|||
}
|
||||
```
|
||||
|
||||
The server uses **OAuth** by default. Your client will discover the authorization server via `/.well-known/oauth-protected-resource` and prompt you to authenticate.
|
||||
|
||||
### API Key Authentication (Alternative)
|
||||
|
||||
If you prefer API keys over OAuth, get one from [app.supermemory.ai](https://app.supermemory.ai) and pass it in the `Authorization` header:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"supermemory": {
|
||||
"url": "https://mcp.supermemory.ai/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer sm_your_api_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
API keys start with `sm_` and skip OAuth when provided.
|
||||
The server requires **OAuth**. Your client will discover the authorization server via `/.well-known/oauth-protected-resource` and prompt you to authenticate.
|
||||
|
||||
### Project Scoping
|
||||
|
||||
|
|
@ -126,4 +107,3 @@ You can access this in Cursor and Claude Code by just doing /context, which will
|
|||
<Card title="MCP Server Source Code" icon="github" href="https://github.com/supermemoryai/supermemory/tree/main/apps/mcp">
|
||||
View the open-source implementation
|
||||
</Card>
|
||||
|
||||
|
|
|
|||
|
|
@ -26,26 +26,7 @@ Add this to your MCP client config (Claude Desktop, Cursor, Windsurf, etc.):
|
|||
}
|
||||
```
|
||||
|
||||
The server uses **OAuth authentication** by default. Your MCP client will automatically discover the authorization server via `/.well-known/oauth-protected-resource` and prompt you to authenticate.
|
||||
|
||||
## API Key Authentication (Alternative)
|
||||
|
||||
If you prefer to use an API key instead of OAuth, get one from [app.supermemory.ai](https://app.supermemory.ai) and pass it in the `Authorization` header:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"supermemory": {
|
||||
"url": "https://mcp.supermemory.ai/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer sm_your_api_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
API keys start with `sm_` and are automatically detected. When an API key is provided, OAuth authentication is skipped.
|
||||
The server requires **OAuth authentication**. Your MCP client will automatically discover the authorization server via `/.well-known/oauth-protected-resource` and prompt you to authenticate.
|
||||
|
||||
## Project Scoping (Optional)
|
||||
|
||||
|
|
@ -90,4 +71,4 @@ Or use the one-click install button at [app.supermemory.ai](https://app.supermem
|
|||
|
||||
### Windsurf / VS Code
|
||||
|
||||
Configuration varies by extension. Generally, add the server URL (`https://mcp.supermemory.ai/mcp`) to your MCP settings.
|
||||
Configuration varies by extension. Generally, add the server URL (`https://mcp.supermemory.ai/mcp`) to your MCP settings.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ A standalone MCP (Model Context Protocol) server for Supermemory that gives AI a
|
|||
|
||||
## Features
|
||||
|
||||
- **Authentication** - Supports both API keys and OAuth authentication
|
||||
- **Authentication** - OAuth 2.1 with dynamic client registration
|
||||
- **Persistent Memory** - Save and recall information across sessions
|
||||
- **User Profiles** - Auto-generated profiles from stored memories
|
||||
- **Project Scoping** - Organize memories by project with `x-sm-project` header
|
||||
|
|
@ -34,26 +34,7 @@ Add to your MCP client config (Claude Desktop, Cursor, Windsurf, etc.):
|
|||
}
|
||||
```
|
||||
|
||||
The server uses OAuth authentication by default. Your MCP client will automatically discover the authorization server via `/.well-known/oauth-protected-resource` and prompt you to authenticate.
|
||||
|
||||
### API Key Authentication (Alternative)
|
||||
|
||||
If you prefer to use an API key instead of OAuth, you can pass it directly in the `Authorization` header. Get your API key from [app.supermemory.ai](https://app.supermemory.ai):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"supermemory": {
|
||||
"url": "https://mcp.supermemory.ai/mcp",
|
||||
"headers": {
|
||||
"Authorization": "Bearer sm_your_api_key_here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
API keys start with `sm_` and are automatically detected. When an API key is provided, OAuth authentication is skipped.
|
||||
The server requires OAuth authentication. Your MCP client will automatically discover the authorization server via `/.well-known/oauth-protected-resource` and prompt you to authenticate.
|
||||
|
||||
### Project Scoping (Optional)
|
||||
|
||||
|
|
@ -196,9 +177,9 @@ core journey: handshake → tool/resource/prompt discovery → `whoAmI` → `lis
|
|||
the `context` prompt, container-tag isolation, and auth rejections.
|
||||
|
||||
```bash
|
||||
export SUPERMEMORY_API_KEY=sm_... # staging key (required; tests skip without it)
|
||||
export SUPERMEMORY_MCP_URL=https://mcp.supermemory.ai/mcp # optional, this is the default
|
||||
export SUPERMEMORY_API_URL=https://api.supermemory.ai # optional, OAuth authorization server
|
||||
bun e2e/capture-oauth-token.ts # one-time browser authorization
|
||||
bun run test:e2e
|
||||
```
|
||||
|
||||
|
|
@ -220,8 +201,8 @@ API (`api.supermemory.ai`, better-auth). `oauth.test.ts` covers the real flow in
|
|||
- **A–C (no secrets)** — discovery chain, dynamic client registration, and token/authorize
|
||||
negatives. These exercise the protocol wiring with no key and no browser, so they always run.
|
||||
- **D (real token)** — exchanges a seeded `refresh_token` for an `access_token` and connects to
|
||||
`/mcp` with it, exercising the OAuth-token validation path (not the `sm_` API-key path). It
|
||||
**skips** unless both env vars below are set.
|
||||
`/mcp` with it, exercising the OAuth-token validation path. It **skips** unless both OAuth
|
||||
credentials below are available.
|
||||
|
||||
```bash
|
||||
# One-time capture (opens a browser for login + consent, prints the env vars):
|
||||
|
|
@ -231,8 +212,8 @@ export SUPERMEMORY_MCP_REFRESH_TOKEN=...
|
|||
```
|
||||
|
||||
Notes:
|
||||
- Tests **skip** (not fail) without `SUPERMEMORY_API_KEY`; Tier D OAuth tests skip without the
|
||||
refresh-token env vars — so CI is safe without secrets.
|
||||
- Authenticated tests **skip** (not fail) without stored OAuth credentials or the refresh-token
|
||||
environment variables, so CI is safe without secrets.
|
||||
- `recall` is eventually-consistent (save → ingestion pipeline → memories), so the round-trip
|
||||
**polls up to ~90s**. `forget` removal is slower still and is asserted as best-effort.
|
||||
- The suite uses unique per-run markers and forgets them in teardown to avoid polluting the account.
|
||||
|
|
@ -246,7 +227,7 @@ bun run deploy
|
|||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ OAuth/API Key ┌──────────────────┐
|
||||
┌─────────────────┐ OAuth ┌──────────────────┐
|
||||
│ MCP Client │◄──────────────►│ Supermemory API │
|
||||
│ (Claude, Cursor)│ │ (api.supermemory.ai)
|
||||
└────────┬────────┘ └──────────────────┘
|
||||
|
|
@ -273,4 +254,3 @@ bun run deploy
|
|||
- **MCP SDK:** @modelcontextprotocol/sdk + agents
|
||||
- **API Client:** supermemory SDK
|
||||
- **Analytics:** PostHog
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const mcpHeaders = (auth?: string) => ({
|
|||
...(auth ? { Authorization: auth } : {}),
|
||||
})
|
||||
|
||||
// No API key needed — exercises the public surface and auth rejections.
|
||||
// No credentials needed — exercises the public surface and auth rejections.
|
||||
describe("MCP — transport & auth (raw HTTP)", () => {
|
||||
it("GET / returns service info", async () => {
|
||||
const res = await fetch(`${ORIGIN}/`)
|
||||
|
|
@ -52,7 +52,7 @@ describe("MCP — transport & auth (raw HTTP)", () => {
|
|||
expect(res.headers.get("www-authenticate")).toMatch(/Bearer/)
|
||||
})
|
||||
|
||||
it("rejects an invalid API key (401 with JSON-RPC error)", async () => {
|
||||
it("rejects an opaque API key as an invalid OAuth token", async () => {
|
||||
const res = await fetch(MCP_URL, {
|
||||
method: "POST",
|
||||
headers: mcpHeaders("Bearer sm_invalid_key_for_e2e"),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { afterAll, beforeAll, describe, expect, it } from "vitest"
|
||||
import {
|
||||
AUTH_CREDENTIALS_AVAILABLE,
|
||||
OAUTH_CREDENTIALS_AVAILABLE,
|
||||
callTool,
|
||||
connect,
|
||||
textOf,
|
||||
|
|
@ -14,7 +14,7 @@ const EXPECTED_TOOLS = [
|
|||
"whoAmI",
|
||||
"memory-graph",
|
||||
]
|
||||
const describeWithAuth = describe.skipIf(!AUTH_CREDENTIALS_AVAILABLE)
|
||||
const describeWithAuth = describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)
|
||||
|
||||
describeWithAuth("MCP — discovery & identity", () => {
|
||||
let s: Session
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { afterAll, beforeAll, describe, expect, it } from "vitest"
|
||||
import {
|
||||
AUTH_CREDENTIALS_AVAILABLE,
|
||||
OAUTH_CREDENTIALS_AVAILABLE,
|
||||
callTool,
|
||||
connect,
|
||||
type Session,
|
||||
textOf,
|
||||
} from "./helpers"
|
||||
const describeWithAuth = describe.skipIf(!AUTH_CREDENTIALS_AVAILABLE)
|
||||
const describeWithAuth = describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)
|
||||
|
||||
describeWithAuth("MCP — graph, resources & prompts", () => {
|
||||
let s: Session
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { fileURLToPath } from "node:url"
|
|||
|
||||
export const MCP_URL =
|
||||
process.env.SUPERMEMORY_MCP_URL ?? "https://mcp.supermemory.ai/mcp"
|
||||
export const API_KEY = process.env.SUPERMEMORY_API_KEY
|
||||
export const ORIGIN = new URL(MCP_URL).origin
|
||||
export const API_URL =
|
||||
process.env.SUPERMEMORY_API_URL ?? "https://api.supermemory.ai"
|
||||
|
|
@ -60,16 +59,15 @@ export const OAUTH_REFRESH_TOKEN =
|
|||
export const OAUTH_CLIENT_ID =
|
||||
process.env.SUPERMEMORY_MCP_CLIENT_ID ??
|
||||
storedCredentials.SUPERMEMORY_MCP_CLIENT_ID
|
||||
export const AUTH_CREDENTIALS_AVAILABLE = Boolean(
|
||||
API_KEY || (OAUTH_REFRESH_TOKEN && OAUTH_CLIENT_ID),
|
||||
export const OAUTH_CREDENTIALS_AVAILABLE = Boolean(
|
||||
OAUTH_REFRESH_TOKEN && OAUTH_CLIENT_ID,
|
||||
)
|
||||
|
||||
let defaultOAuthAccessToken: Promise<string> | undefined
|
||||
|
||||
async function defaultBearerToken(): Promise<string> {
|
||||
if (API_KEY) return API_KEY
|
||||
if (!OAUTH_REFRESH_TOKEN || !OAUTH_CLIENT_ID) {
|
||||
throw new Error("No API key or OAuth test credentials configured")
|
||||
throw new Error("No OAuth test credentials configured")
|
||||
}
|
||||
|
||||
defaultOAuthAccessToken ??= (async () => {
|
||||
|
|
@ -185,9 +183,9 @@ export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|||
export type Session = { client: Client; close: () => Promise<void> }
|
||||
|
||||
export async function connect(
|
||||
opts: { apiKey?: string; token?: string; containerTag?: string } = {},
|
||||
opts: { token?: string; containerTag?: string } = {},
|
||||
): Promise<Session> {
|
||||
const bearerToken = opts.token ?? opts.apiKey ?? (await defaultBearerToken())
|
||||
const bearerToken = opts.token ?? (await defaultBearerToken())
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { afterAll, beforeAll, describe, expect, it } from "vitest"
|
||||
import {
|
||||
AUTH_CREDENTIALS_AVAILABLE,
|
||||
OAUTH_CREDENTIALS_AVAILABLE,
|
||||
callTool,
|
||||
connect,
|
||||
type Session,
|
||||
textOf,
|
||||
} from "./helpers"
|
||||
|
||||
describe.skipIf(!AUTH_CREDENTIALS_AVAILABLE)("MCP — listMemories", () => {
|
||||
describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)("MCP — listMemories", () => {
|
||||
let s: Session
|
||||
|
||||
beforeAll(async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { randomUUID } from "node:crypto"
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest"
|
||||
import {
|
||||
AUTH_CREDENTIALS_AVAILABLE,
|
||||
OAUTH_CREDENTIALS_AVAILABLE,
|
||||
callTool,
|
||||
connect,
|
||||
recallUntil,
|
||||
|
|
@ -9,7 +9,7 @@ import {
|
|||
textOf,
|
||||
} from "./helpers"
|
||||
|
||||
describe.skipIf(!AUTH_CREDENTIALS_AVAILABLE)("MCP — memory behaviors", () => {
|
||||
describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)("MCP — memory behaviors", () => {
|
||||
let s: Session
|
||||
const created: Array<{ content: string; containerTag?: string }> = []
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ describe("MCP — OAuth protocol (no secrets)", () => {
|
|||
})
|
||||
})
|
||||
|
||||
// Tier D — real OAuth token through /mcp, exercising validateOAuthToken (not the sm_ branch); needs a seeded refresh token.
|
||||
// Tier D — real OAuth token through /mcp; needs a seeded refresh token.
|
||||
describe.skipIf(!OAUTH_REFRESH_TOKEN || !OAUTH_CLIENT_ID)(
|
||||
"MCP — real OAuth token round-trip",
|
||||
() => {
|
||||
|
|
@ -122,8 +122,8 @@ describe.skipIf(!OAUTH_REFRESH_TOKEN || !OAUTH_CLIENT_ID)(
|
|||
await s?.close()
|
||||
})
|
||||
|
||||
it("mints an OAuth access token that is not an sm_ API key", () => {
|
||||
expect(accessToken.startsWith("sm_")).toBe(false)
|
||||
it("mints a JWT access token for the MCP resource", () => {
|
||||
expect(accessToken.split(".")).toHaveLength(3)
|
||||
})
|
||||
|
||||
it("connects to /mcp with the OAuth token and resolves identity", async () => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { randomUUID } from "node:crypto"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import {
|
||||
AUTH_CREDENTIALS_AVAILABLE,
|
||||
OAUTH_CREDENTIALS_AVAILABLE,
|
||||
callTool,
|
||||
connect,
|
||||
recallUntil,
|
||||
|
|
@ -18,7 +18,7 @@ const propsOf = (tools: ToolLike[], name: string): Record<string, unknown> =>
|
|||
|
||||
// Fixed tag (not a per-run UUID) so the test doesn't mint a new project each run.
|
||||
const SCOPE_TAG = "sm_e2e_root"
|
||||
const describeWithAuth = describe.skipIf(!AUTH_CREDENTIALS_AVAILABLE)
|
||||
const describeWithAuth = describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)
|
||||
|
||||
// x-sm-project locks the connection to one project: strips containerTag from schemas and scopes every op — distinct from the per-call arg.
|
||||
describeWithAuth("MCP — x-sm-project root scoping", () => {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { afterAll, beforeAll, describe, expect, it } from "vitest"
|
||||
import {
|
||||
AUTH_CREDENTIALS_AVAILABLE,
|
||||
OAUTH_CREDENTIALS_AVAILABLE,
|
||||
callTool,
|
||||
connect,
|
||||
type Session,
|
||||
} from "./helpers"
|
||||
|
||||
describe.skipIf(!AUTH_CREDENTIALS_AVAILABLE)(
|
||||
describe.skipIf(!OAUTH_CREDENTIALS_AVAILABLE)(
|
||||
"MCP - on-demand widget permissions",
|
||||
() => {
|
||||
let session: Session
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createLocalJWKSet, exportJWK, generateKeyPair, SignJWT } from "jose"
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest"
|
||||
import { fetchSession, validateApiKey, validateOAuthToken } from "./index"
|
||||
import { fetchSession, validateOAuthToken } from "./index"
|
||||
|
||||
const API_URL = "https://api.example.com"
|
||||
const ISSUER = `${API_URL}/api/auth`
|
||||
|
|
@ -73,20 +73,15 @@ describe("MCP authentication", () => {
|
|||
).resolves.toBeNull()
|
||||
})
|
||||
|
||||
it("introspects opaque API keys through v3/session", async () => {
|
||||
const fetchSpy = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ user: { id: "user_api_key" } }), {
|
||||
status: 200,
|
||||
}),
|
||||
)
|
||||
it("rejects opaque API keys without an API request", async () => {
|
||||
vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
const fetchSpy = vi.fn()
|
||||
vi.stubGlobal("fetch", fetchSpy)
|
||||
|
||||
await expect(validateApiKey("sm_test", `${API_URL}/`)).resolves.toEqual({
|
||||
userId: "user_api_key",
|
||||
bearerToken: "sm_test",
|
||||
})
|
||||
expect(fetchSpy).toHaveBeenCalledOnce()
|
||||
expect(fetchSpy.mock.calls[0][0]).toBe(`${API_URL}/v3/session`)
|
||||
await expect(
|
||||
validateOAuthToken("sm_test", API_URL, MCP_RESOURCE, keySet),
|
||||
).resolves.toBeNull()
|
||||
expect(fetchSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("surfaces on-demand session failures to the calling tool", async () => {
|
||||
|
|
|
|||
|
|
@ -10,10 +10,6 @@ export interface AuthUser {
|
|||
|
||||
const remoteJwks = new Map<string, ReturnType<typeof createRemoteJWKSet>>()
|
||||
|
||||
export function isApiKey(token: string): boolean {
|
||||
return token.startsWith("sm_")
|
||||
}
|
||||
|
||||
function authIssuer(apiUrl: string): string {
|
||||
return `${apiUrl.replace(/\/+$/, "")}/api/auth`
|
||||
}
|
||||
|
|
@ -52,22 +48,6 @@ export async function fetchSession(
|
|||
return session
|
||||
}
|
||||
|
||||
export async function validateApiKey(
|
||||
apiKey: string,
|
||||
apiUrl: string,
|
||||
): Promise<AuthUser | null> {
|
||||
try {
|
||||
const session = await fetchSession(apiKey, apiUrl)
|
||||
return {
|
||||
userId: session.user.id,
|
||||
bearerToken: apiKey,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("API key validation error:", error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateOAuthToken(
|
||||
token: string,
|
||||
apiUrl: string,
|
||||
|
|
|
|||
|
|
@ -3,12 +3,7 @@ import { cors } from "hono/cors"
|
|||
import type { ContentfulStatusCode } from "hono/utils/http-status"
|
||||
import type { Props } from "../shared/types"
|
||||
import { SupermemoryMCP } from "./agent"
|
||||
import {
|
||||
type AuthUser,
|
||||
isApiKey,
|
||||
validateApiKey,
|
||||
validateOAuthToken,
|
||||
} from "./auth"
|
||||
import { validateOAuthToken } from "./auth"
|
||||
|
||||
type Bindings = {
|
||||
MCP_SERVER: DurableObjectNamespace
|
||||
|
|
@ -16,16 +11,6 @@ type Bindings = {
|
|||
MCP_RESOURCE?: string
|
||||
}
|
||||
|
||||
async function resolveAuth(
|
||||
token: string,
|
||||
apiUrl: string,
|
||||
mcpResource: string,
|
||||
): Promise<AuthUser | null> {
|
||||
return isApiKey(token)
|
||||
? await validateApiKey(token, apiUrl)
|
||||
: await validateOAuthToken(token, apiUrl, mcpResource)
|
||||
}
|
||||
|
||||
export type { Props }
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>()
|
||||
|
|
@ -141,7 +126,7 @@ async function handleMcpRequest(
|
|||
})
|
||||
}
|
||||
|
||||
const authUser = await resolveAuth(token, apiUrl, mcpResource)
|
||||
const authUser = await validateOAuthToken(token, apiUrl, mcpResource)
|
||||
|
||||
if (!authUser) {
|
||||
return new Response(
|
||||
|
|
@ -149,9 +134,7 @@ async function handleMcpRequest(
|
|||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32000,
|
||||
message: isApiKey(token)
|
||||
? "Invalid or expired API key"
|
||||
: "Invalid or expired token",
|
||||
message: "Invalid or expired token",
|
||||
},
|
||||
id: null,
|
||||
}),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue