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
92 changes: 91 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,93 @@ The MCP server provides the following tools:
| `symbol_analysis` | Analyzes a symbol and returns a report of its definitions, call sites, and references. This is useful for understanding the role of a symbol in the codebase. |
| `read_file_from_chunks` | Reads the content of a file from the index, providing a reconstructed view based on the most important indexed chunks. |
| `document_symbols` | Analyzes a file to identify the key symbols that would most benefit from documentation. This is useful for automating the process of improving the semantic quality of a codebase. |
| `auth_status` | Returns your current OAuth authentication status: client ID, granted scopes, and token expiry. Only available when `MCP_OAUTH_ENABLED=true`. Never includes the token value. |

**Note:** All of the tools accept an optional `index` parameter that allows you to override the `ELASTICSEARCH_INDEX` for a single query.

---

## OAuth 2.0 Authentication (HTTP mode)

The HTTP server supports OAuth 2.0 bearer token authentication. When enabled, MCP clients (Claude Code, VS Code, Cursor) automatically discover the authorization server, obtain a token, and present it on every request. The server only validates tokens — it never issues them.

### Prerequisites

- An OIDC-compliant authorization server (Okta, Auth0, Keycloak, etc.)
- **The server must be reachable at its own dedicated (sub)domain.** MCP clients fetch `/.well-known/oauth-protected-resource` from the root of the server's domain to discover the authorization server. This well-known URI must resolve at the domain root per [RFC 8615](https://www.rfc-editor.org/rfc/rfc8615) section 3 and [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) section 3. A subpath deployment (e.g. `https://shared.example.com/my-mcp`) will not work.
- **Okta app type must be SPA (not Web).** MCP clients use the Authorization Code + PKCE flow ([RFC 7636](https://www.rfc-editor.org/rfc/rfc7636)) without a client secret. Web app type requires a client secret for code exchange and will fail.

### JWKS validation (default — no secrets required)

The server validates JWTs locally using the provider's public JWKS endpoint discovered from the issuer's OIDC configuration.

```bash
MCP_OAUTH_ENABLED=true
MCP_OAUTH_ISSUER=https://your-okta.okta.com/oauth2/default
MCP_SERVER_URL=https://your-server.example.com # must be the server's public URL
# Optional:
MCP_OAUTH_AUDIENCE=api://default # for Okta non-URL audience strings
MCP_OAUTH_REQUIRED_SCOPES=openid # space-separated; minimum "openid" for Okta
```

### Token introspection (opt-in — requires client credentials)

Activated when both `MCP_OAUTH_CLIENT_ID` and `MCP_OAUTH_CLIENT_SECRET` are set. The server calls the provider's [RFC 7662](https://www.rfc-editor.org/rfc/rfc7662) introspection endpoint on every request. Use this when the provider issues opaque (non-JWT) tokens, or when real-time revocation checking is required.

```bash
MCP_OAUTH_ENABLED=true
MCP_OAUTH_ISSUER=https://your-keycloak.com/realms/myrealm
MCP_OAUTH_CLIENT_ID=my-resource-server
MCP_OAUTH_CLIENT_SECRET=super-secret
MCP_SERVER_URL=https://your-server.example.com
```

### Docker (HTTP mode with OAuth)

```bash
docker run --rm -p 3000:3000 \
-e ELASTICSEARCH_ENDPOINT=https://... \
-e MCP_OAUTH_ENABLED=true \
-e MCP_OAUTH_ISSUER=https://your-okta.okta.com/oauth2/default \
-e MCP_SERVER_URL=https://your-server.example.com \
-e MCP_OAUTH_REQUIRED_SCOPES=openid \
simianhacker/semantic-code-search-mcp-server
```

### Required scopes

`MCP_OAUTH_REQUIRED_SCOPES` controls which scopes the server advertises and requires on every token. The minimum recommended value for Okta is `openid`. Setting it to an empty string causes Okta to reject the authorization request with a "no scopes configured" error.

Note: scopes like `offline_access` and `email` work with Okta and the major IDEs but are not guaranteed by any standard. Avoid them if you need M2M (client credentials) access.

### Local development without OAuth

When `MCP_SERVER_URL` is not set (or points to localhost), the server binds to `127.0.0.1` only. Set `MCP_SERVER_URL` to a non-localhost URL to bind to all interfaces (required for Docker containers and reverse proxy deployments).

### Restricting access to specific OAuth clients

By default, any token issued by the configured authorization server is accepted. To restrict access to a specific app:

```bash
MCP_OAUTH_ALLOWED_CLIENT_IDS=0oa1abc123def456gh78 # space-separated for multiple IDs
```

This is recommended when multiple OAuth apps share the same authorization server (common in Okta). Without it, tokens from any app in the tenant that targets the same audience will be accepted. The server checks the `client_id`, `azp`, or `cid` (Okta-specific) claim in the JWT, whichever is present.

### Checking your auth status

When OAuth is enabled, an `auth_status` tool is available in all MCP clients. Ask the AI assistant to call it:

> "Call the auth_status tool"

It returns your client ID, granted scopes, and token expiry — nothing sensitive (the token itself is never included).

### Token lifetime

Clients re-authenticate when their access token expires. To reduce auth prompts, increase the access token lifetime in your authorization server. For Okta: Admin → Security → API → Authorization Servers → default → Access Policies.

---

## Configuration

Configuration is managed via environment variables in a `.env` file.
Expand All @@ -204,4 +286,12 @@ Configuration is managed via environment variables in a `.env` file.
| --- | --- | --- |
| `ELASTICSEARCH_CLOUD_ID` | The Cloud ID for your Elastic Cloud instance. | |
| `ELASTICSEARCH_API_KEY` | An API key for Elasticsearch authentication. | |
| `ELASTICSEARCH_INDEX` | The name of the Elasticsearch index to use. | `semantic-code-search` |
| `ELASTICSEARCH_INDEX` | The name of the Elasticsearch index to use. | `semantic-code-search` |
| `MCP_OAUTH_ENABLED` | Enable OAuth 2.0 bearer token authentication (HTTP mode only). | `false` |
| `MCP_OAUTH_ISSUER` | OIDC issuer URL. Required when `MCP_OAUTH_ENABLED=true`. | |
| `MCP_OAUTH_AUDIENCE` | Expected `aud` claim override. Use for Okta non-URL audiences (e.g. `api://default`). | |
| `MCP_OAUTH_REQUIRED_SCOPES` | Space-separated scopes the server requires on every token. Minimum `openid` for Okta. | |
| `MCP_OAUTH_ALLOWED_CLIENT_IDS` | Space-separated allowlist of OAuth client IDs. Empty = any client from the issuer. | |
| `MCP_OAUTH_CLIENT_ID` | Client ID for token introspection (opt-in). Requires `MCP_OAUTH_CLIENT_SECRET`. | |
| `MCP_OAUTH_CLIENT_SECRET` | Client secret for token introspection (opt-in). | |
| `MCP_SERVER_URL` | Public URL of the server. Required for OAuth and non-localhost deployments. | |
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ module.exports = {
watchman: false,
testMatch: ['**/tests/**/*.test.ts'],
modulePathIgnorePatterns: ['<rootDir>/.repos/'],
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
};
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@types/moment-timezone": "^0.5.13",
"dotenv": "^17.2.1",
"express": "^5.1.0",
"jose": "^5.10.0",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0"
Expand Down
21 changes: 21 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,24 @@ export const elasticsearchConfig = {
apiKey: process.env.ELASTICSEARCH_API_KEY,
index: process.env.ELASTICSEARCH_INDEX || 'semantic-code-search',
};

export const oauthConfig = {
enabled: process.env.MCP_OAUTH_ENABLED === 'true',
issuer: process.env.MCP_OAUTH_ISSUER,
clientId: process.env.MCP_OAUTH_CLIENT_ID,
clientSecret: process.env.MCP_OAUTH_CLIENT_SECRET,
// Optional: override the expected `aud` claim. Needed for OIDC providers (e.g. Okta)
// that use a fixed audience (e.g. "api://default") instead of the resource URL.
audience: process.env.MCP_OAUTH_AUDIENCE,
requiredScopes: process.env.MCP_OAUTH_REQUIRED_SCOPES
? process.env.MCP_OAUTH_REQUIRED_SCOPES.split(' ').filter(Boolean)
: [],
// Optional: restrict which OAuth client IDs are allowed to access this server.
// Space-separated list. If empty, any client from the configured issuer is accepted.
// Set to your Okta app's client ID to prevent tokens from other apps in the same
// tenant from being accepted.
allowedClientIds: process.env.MCP_OAUTH_ALLOWED_CLIENT_IDS
? process.env.MCP_OAUTH_ALLOWED_CLIENT_IDS.split(' ').filter(Boolean)
: [],
serverUrl: process.env.MCP_SERVER_URL,
};
Loading