make mcp oauth only

This commit is contained in:
Prasanna A P 2026-07-20 20:32:31 -07:00
parent 2c6d5f36ed
commit 2d920c12f0
21 changed files with 49 additions and 221 deletions

View file

@ -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"

View file

@ -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.

View file

@ -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"

View file

@ -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)

View file

@ -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 构建

View file

@ -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

View file

@ -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>

View file

@ -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.

View file

@ -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

View file

@ -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"),

View file

@ -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

View file

@ -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

View file

@ -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}`,
}

View file

@ -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 () => {

View file

@ -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 }> = []

View file

@ -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 () => {

View file

@ -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", () => {

View file

@ -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

View file

@ -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 () => {

View file

@ -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,

View file

@ -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,
}),