-
Notifications
You must be signed in to change notification settings - Fork 239
Account management MCP #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
harshil1712
wants to merge
4
commits into
cloudflare:main
Choose a base branch
from
harshil1712:account-management-mcp
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+6,496
−0
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
CLOUDFLARE_CLIENT_ID= | ||
CLOUDFLARE_CLIENT_SECRET= | ||
DEV_DISABLE_OAUTH= | ||
DEV_CLOUDFLARE_API_TOKEN= | ||
DEV_CLOUDFLARE_EMAIL= |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/** @type {import("eslint").Linter.Config} */ | ||
module.exports = { | ||
root: true, | ||
extends: ['@repo/eslint-config/default.cjs'], | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# Setup | ||
|
||
If you'd like to iterate and test your MCP server, you can do so in local development. | ||
|
||
## Local Development | ||
|
||
1. Create a `.dev.vars` file in your project root: | ||
|
||
If you're a Cloudflare employee: | ||
|
||
``` | ||
CLOUDFLARE_CLIENT_ID=your_development_cloudflare_client_id | ||
CLOUDFLARE_CLIENT_SECRET=your_development_cloudflare_client_secret | ||
``` | ||
|
||
If you're an external contributor, you can provide a development API token: | ||
|
||
``` | ||
DEV_DISABLE_OAUTH=true | ||
# This is your global api token | ||
DEV_CLOUDFLARE_API_TOKEN=your_development_api_token | ||
``` | ||
|
||
2. Start the local development server: | ||
|
||
```bash | ||
npx wrangler dev | ||
``` | ||
|
||
3. To test locally, open Inspector, and connect to `http://localhost:8976/sse`. | ||
Once you follow the prompts, you'll be able to "List Tools". You can also connect with any MCP client. | ||
|
||
## Deploying the Worker ( Cloudflare employees only ) | ||
|
||
Set secrets via Wrangler: | ||
|
||
```bash | ||
npx wrangler secret put CLOUDFLARE_CLIENT_ID -e <ENVIRONMENT> | ||
npx wrangler secret put CLOUDFLARE_CLIENT_SECRET -e <ENVIRONMENT> | ||
``` | ||
|
||
## Set up a KV namespace | ||
|
||
Create the KV namespace: | ||
|
||
```bash | ||
npx wrangler kv namespace create "OAUTH_KV" | ||
``` | ||
|
||
Then, update the Wrangler file with the generated KV namespace ID. | ||
|
||
## Deploy & Test | ||
|
||
Deploy the MCP server to make it available on your workers.dev domain: | ||
|
||
```bash | ||
npx wrangler deploy -e <ENVIRONMENT> | ||
``` | ||
|
||
Test the remote server using [Inspector](https://modelcontextprotocol.io/docs/tools/inspector): | ||
|
||
```bash | ||
npx @modelcontextprotocol/inspector@latest | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# Cloudflare Account Management MCP Server 📡 | ||
|
||
This is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server that supports remote MCP | ||
connections, with Cloudflare OAuth built-in. | ||
|
||
It integrates the [Cloudflare Account Members API](https://developers.cloudflare.com/api/node/resources/accounts/subresources/members/) to allow you to manage your Cloudflare account members. | ||
|
||
## 🔨 Available Tools | ||
|
||
Currently available tools: | ||
|
||
| **Tool** | **Description** | | ||
| ---------------------- | ---------------------------------------------------------------------------- | | ||
| `get_members_in_account` | Retrieves the members in the specified account. | | ||
| `get_roles` | Fetches the roles in the specified account. | | ||
| `invite_to_account` | Invites a user to the specified account. NOTE: You will have to use the email address of the user you want to invite. | | ||
| `update_member` | Updates a member in the specified account. | | ||
| `remove_member` | Removes a member from the specified account. | | ||
|
||
**Note:** To use these tools, ensure you have an active account set. If not, use `accounts_list` to list your accounts and `set_active_account` to set one as active. | ||
|
||
This MCP server is still a work in progress, and we plan to add more tools in the future. | ||
|
||
### Prompt Examples | ||
|
||
- `Get the members in my account.` | ||
- `Get the roles in my account.` | ||
- `Invite a user to my account.` | ||
- `Update a member in my account.` | ||
- `Remove a member from my account.` | ||
|
||
## Access the remote MCP server from any MCP Client | ||
|
||
If your MCP client has first class support for remote MCP servers, the client will provide a way to accept the server URL (`https://account-management.mcp.cloudflare.com`) directly within its interface (for example in[Cloudflare AI Playground](https://playground.ai.cloudflare.com/)). | ||
|
||
If your client does not yet support remote MCP servers, you will need to set up its resepective configuration file using mcp-remote (https://www.npmjs.com/package/mcp-remote) to specify which servers your client can access. | ||
|
||
Replace the content with the following configuration: | ||
|
||
```json | ||
{ | ||
"mcpServers": { | ||
"account-management": { | ||
"command": "npx", | ||
"args": ["mcp-remote", "https://account-management.mcp.cloudflare.com/sse"] | ||
} | ||
} | ||
} | ||
``` | ||
|
||
Once you've set up your configuration file, restart MCP client and a browser window will open showing your OAuth login page. Proceed through the authentication flow to grant the client access to your MCP server. After you grant access, the tools will become available for you to use. | ||
|
||
Interested in contributing, and running this server locally? See [CONTRIBUTING.md](CONTRIBUTING.md) to get started. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"name": "cloudflare-account-management-mcp-server", | ||
"version": "0.0.1", | ||
"private": true, | ||
"scripts": { | ||
"check:lint": "run-eslint-workers", | ||
"check:types": "run-tsc", | ||
"deploy": "run-wrangler-deploy", | ||
"dev": "wrangler dev", | ||
"start": "wrangler dev", | ||
"types": "wrangler types --include-env=false", | ||
"test": "vitest run" | ||
}, | ||
"dependencies": { | ||
"@cloudflare/workers-oauth-provider": "0.0.5", | ||
"@hono/zod-validator": "0.4.3", | ||
"@modelcontextprotocol/sdk": "1.10.2", | ||
"@repo/mcp-common": "workspace:*", | ||
"@repo/mcp-observability": "workspace:*", | ||
"agents": "0.0.67", | ||
"cloudflare": "4.2.0", | ||
"hono": "4.7.6", | ||
"zod": "3.24.2" | ||
}, | ||
"devDependencies": { | ||
"@cloudflare/vitest-pool-workers": "0.8.14", | ||
"@types/node": "22.14.1", | ||
"prettier": "3.5.3", | ||
"typescript": "5.5.4", | ||
"vitest": "3.0.9", | ||
"wrangler": "4.10.0" | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import type { UserDetails } from '@repo/mcp-common/src/durable-objects/user_details' | ||
import type { AccountManagementMCP } from './index' | ||
|
||
export interface Env { | ||
OAUTH_KV: KVNamespace | ||
ENVIRONMENT: 'development' | 'staging' | 'production' | ||
MCP_SERVER_NAME: string | ||
MCP_SERVER_VERSION: string | ||
CLOUDFLARE_CLIENT_ID: string | ||
CLOUDFLARE_CLIENT_SECRET: string | ||
MCP_OBJECT: DurableObjectNamespace<AccountManagementMCP> | ||
USER_DETAILS: DurableObjectNamespace<UserDetails> | ||
MCP_METRICS: AnalyticsEngineDataset | ||
DEV_DISABLE_OAUTH: string | ||
DEV_CLOUDFLARE_API_TOKEN: string | ||
DEV_CLOUDFLARE_EMAIL: string | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import OAuthProvider from '@cloudflare/workers-oauth-provider' | ||
import { McpAgent } from 'agents/mcp' | ||
|
||
import { | ||
createAuthHandlers, | ||
handleTokenExchangeCallback, | ||
} from '@repo/mcp-common/src/cloudflare-oauth-handler' | ||
import { handleDevMode } from '@repo/mcp-common/src/dev-mode' | ||
import { getUserDetails, UserDetails } from '@repo/mcp-common/src/durable-objects/user_details' | ||
import { getEnv } from '@repo/mcp-common/src/env' | ||
import { RequiredScopes } from '@repo/mcp-common/src/scopes' | ||
import { CloudflareMCPServer } from '@repo/mcp-common/src/server' | ||
import { registerAccountTools } from '@repo/mcp-common/src/tools/account' | ||
|
||
import { MetricsTracker } from '../../../packages/mcp-observability/src' | ||
import { registerAccountManagementTools } from './tools/accountManagement' | ||
|
||
import type { AuthProps } from '@repo/mcp-common/src/cloudflare-oauth-handler' | ||
import type { Env } from './context' | ||
|
||
const env = getEnv<Env>() | ||
|
||
export { UserDetails } | ||
|
||
const metrics = new MetricsTracker(env.MCP_METRICS, { | ||
name: env.MCP_SERVER_NAME, | ||
version: env.MCP_SERVER_VERSION, | ||
}) | ||
|
||
// Context from the auth process, encrypted & stored in the auth token | ||
// and provided to the DurableMCP as this.props | ||
type Props = AuthProps | ||
type State = { activeAccountId: string | null } | ||
|
||
export class AccountManagementMCP extends McpAgent<Env, State, Props> { | ||
_server: CloudflareMCPServer | undefined | ||
set server(server: CloudflareMCPServer) { | ||
this._server = server | ||
} | ||
get server(): CloudflareMCPServer { | ||
if (!this._server) { | ||
throw new Error('Tried to access server before it was initialized') | ||
} | ||
|
||
return this._server | ||
} | ||
|
||
constructor(ctx: DurableObjectState, env: Env) { | ||
super(ctx, env) | ||
} | ||
|
||
async init() { | ||
this.server = new CloudflareMCPServer({ | ||
userId: this.props.user.id, | ||
wae: this.env.MCP_METRICS, | ||
serverInfo: { | ||
name: this.env.MCP_SERVER_NAME, | ||
version: this.env.MCP_SERVER_VERSION, | ||
}, | ||
}) | ||
|
||
registerAccountTools(this) | ||
|
||
registerAccountManagementTools(this) | ||
} | ||
|
||
async getActiveAccountId() { | ||
try { | ||
// Get UserDetails Durable Object based off the userId and retrieve the activeAccountId from it | ||
// we do this so we can persist activeAccountId across sessions | ||
const userDetails = getUserDetails(env, this.props.user.id) | ||
return await userDetails.getActiveAccountId() | ||
} catch (e) { | ||
this.server.recordError(e) | ||
return null | ||
} | ||
} | ||
|
||
async setActiveAccountId(accountId: string) { | ||
try { | ||
const userDetails = getUserDetails(env, this.props.user.id) | ||
await userDetails.setActiveAccountId(accountId) | ||
} catch (e) { | ||
this.server.recordError(e) | ||
} | ||
} | ||
} | ||
|
||
const AccountManagementScopes = { | ||
...RequiredScopes, | ||
'account:read': 'See your account info such as account details, analytics, and memberships.', | ||
'account:write': 'Manage your account info such as account details, analytics, and memberships.', | ||
} as const | ||
|
||
export default { | ||
fetch: async (req: Request, env: Env, ctx: ExecutionContext) => { | ||
if (env.ENVIRONMENT === 'development' && env.DEV_DISABLE_OAUTH === 'true') { | ||
return await handleDevMode(AccountManagementMCP, req, env, ctx) | ||
} | ||
|
||
return new OAuthProvider({ | ||
apiHandlers: { | ||
'/mcp': AccountManagementMCP.serve('/mcp'), | ||
'/sse': AccountManagementMCP.serveSSE('/sse'), | ||
}, | ||
// @ts-ignore | ||
defaultHandler: createAuthHandlers({ scopes: AccountManagementScopes, metrics }), | ||
authorizeEndpoint: '/oauth/authorize', | ||
tokenEndpoint: '/token', | ||
tokenExchangeCallback: (options) => | ||
handleTokenExchangeCallback( | ||
options, | ||
env.CLOUDFLARE_CLIENT_ID, | ||
env.CLOUDFLARE_CLIENT_SECRET | ||
), | ||
// Cloudflare access token TTL | ||
accessTokenTTL: 3600, | ||
clientRegistrationEndpoint: '/register', | ||
}).fetch(req, env, ctx) | ||
}, | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to request both account:read and account:write, or can we just request account:write?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the latter would be enough. I have a ticket open, will see what the team says.