-
Notifications
You must be signed in to change notification settings - Fork 67
@W-20976712: Add MCP OAuth scope support and enforcement #205
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
base: main
Are you sure you want to change the base?
Conversation
|
Thanks for the contribution! Unfortunately we can't verify the commit author(s): mfilbert <m***@s***.com>. One possible solution is to add that email to your GitHub account. Alternatively you can change your commits to another email and force push the change. After getting your commits associated with your GitHub account, refresh the status of this Pull Request. |
a6ca30b to
01c9f2e
Compare
baa916b to
41415fa
Compare
| export type McpScope = | ||
| | 'tableau:content:read' | ||
| | 'tableau:viz_data_service:read' | ||
| | 'tableau:views:download' | ||
| | 'tableau:insight_definitions_metrics:read' | ||
| | 'tableau:insight_metrics:read' | ||
| | 'tableau:metric_subscriptions:read' | ||
| | 'tableau:insights:read' | ||
| | 'tableau:insight_brief:create'; |
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.
In my mind, I was thinking we wouldn't use an API scope to protect an MCP resource directly ("resource" as in OAuth resource, not MCP resource). That is: an access token must have, say, the tableau:mcp:datasource:read MCP scope to even call the query-datasource tool, but that same access token would also need the 'tableau:viz_data_service:read' API scope since it's going to be re-used to call into VDS.
src/server/oauth/scopes.ts
Outdated
| * Validates that a scope string is a valid MCP scope | ||
| */ | ||
| export function isValidScope(scope: string): scope is McpScope { | ||
| return DEFAULT_SCOPES_SUPPORTED.includes(scope as McpScope); |
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.
You can avoid the type assertion using DEFAULT_SCOPES_SUPPORTED.some((s) => s === scope);
src/server/oauth/scopes.ts
Outdated
| * @param endpoint - The MCP endpoint or tool name | ||
| * @returns Array of required scopes for the endpoint | ||
| */ | ||
| export function getRequiredScopesForTool(toolName: ToolName | string): string[] { |
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.
Since we are going to need to maintain this map, let's use it as the source of truth for when tools generate a JWT with scopes.
For example, the query-datasource tool currently passes jwtScopes: ['tableau:viz_data_service:read'], to useRestApi().
Instead, it could pass: jwtScopes: getRequiredScopesForTool(queryDatasourceTool.name),.
This way we can never accidentally forget to update this map or the tool implementation.
src/server/oauth/scopes.ts
Outdated
| * @returns Array of required scopes for the endpoint | ||
| */ | ||
| export function getRequiredScopesForTool(toolName: ToolName | string): string[] { | ||
| const toolScopeMap: Record<ToolName, McpScope[]> = { |
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.
Let's pull toolScopeMap outside of the function definition. It doesn't need to be initialized every time the function is called.
src/server/oauth/scopes.ts
Outdated
| * @param endpoint - The MCP endpoint or tool name | ||
| * @returns Array of required scopes for the endpoint | ||
| */ | ||
| export function getRequiredScopesForTool(toolName: ToolName | string): string[] { |
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.
Let's change the type on the toolName parameter to be ToolName. Calling sites can provide their tool name using queryDatasourceTool.name (or even this.name if we call this function from tool.ts) for example. Then we don't need the if (toolName in toolScopeMap) or the fallback return DEFAULT_REQUIRED_SCOPES; below.
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.
Also were you planning to wire this function up in a followup PR?
src/server/oauth/scopes.ts
Outdated
| * @param endpoint - The MCP endpoint or tool name | ||
| * @returns Array of required scopes for the endpoint | ||
| */ | ||
| export function getRequiredScopesForTool(toolName: ToolName | string): string[] { |
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.
If scopes are disabled, getRequiredScopesForTool should return an empty array? Or were you planning to not call this function when scopes are disabled?
| code_challenge_methods_supported: ['S256'], | ||
| scopes_supported: [], | ||
| scopes_supported: scopesSupported, | ||
| token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post'], |
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.
While we're here, would you mind making a driveby change here and add 'none' to token_endpoint_auth_methods_supported?
https://salesforce-internal.slack.com/archives/C08QYBH8AE5/p1769075292144669
| For the initial release, use an inclusive scope set (MCP + Tableau API scopes) to avoid token | ||
| exchange. This keeps consent simple and aligns with the current MCP server behavior. |
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.
Internal decisions and implementation details don't need to be documented.
| - Not all MCP tools call Tableau APIs. Some tools can operate entirely within the MCP server | ||
| (for example, generating a TWB). Tableau API scopes do not describe those operations. | ||
| - MCP also exposes non-API concepts like prompts and resources that should be gated behind MCP | ||
| scopes. Those do not have a natural Tableau API scope equivalent. | ||
| - Keeping MCP scopes separate from API scopes clarifies intent and avoids over-granting: MCP scopes | ||
| authorize what the MCP server can do; Tableau API scopes authorize what downstream APIs can do. |
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 don't think we need to go into these details in the docs. We should describe more of the "what", and not necessarily the "why" here.
src/server/oauth/authorize.ts
Outdated
| if (scopesToGrant.length > 0) { | ||
| oauthUrl.searchParams.set('scope', formatScopes(scopesToGrant)); | ||
| } |
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.
So, the /oauth2/v1/auth endpoint on Tableau doesn't support scopes or consent. Not sure it makes sense to provide the scopes if they're going to be ignored.
| 3. **Token Exchange**: | ||
| - What is the endpoint for token exchange? | ||
| - What format should the request/response be? | ||
| - How do we convert MCP tokens to Tableau REST API tokens? |
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.
We aren't going to use token exchange for initial release
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.
thank you for the reminder - I'm going through all of your great feedback now
655038b to
c4ebe90
Compare
src/config.ts
Outdated
| TELEMETRY_PROVIDER: _telemetryProvider, | ||
| TELEMETRY_PROVIDER_CONFIG: _telemetryProviderConfig, |
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.
Was the rename of the telemetryProvider and telemetryProviderConfig variables intentional?
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.
Accident after linting + merge. Will revert
src/restApiInstance.ts
Outdated
| | 'tableau:insights:read' | ||
| | 'tableau:views:download' | ||
| | 'tableau:insight_brief:create'; | ||
| type JwtScopes = TableauApiScope; |
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.
Let's kill the JwtScopes type entirely.
Description
Implemented MCP OAuth scope support and best‑practice authorization challenges. Added scope config knobs, advertised scopes_supported, validated requested scopes, persisted scopes across auth/refresh flows, embedded scopes in access tokens, and enforced required scopes with insufficient_scope challenges. Updated OAuth docs with a recommended beta scope set and rationale for MCP scopes.
Motivation and Context
MCP lacked spec‑aligned scope handling and challenge responses, which blocked least‑privilege access and step‑up behavior. This change brings OAuth behavior in line with the MCP authorization spec and provides a clear scope configuration for beta.
Type of Change
How Has This Been Tested?
Not run (logic is unit‑level; tests can be added in a follow‑up).
Related Issues
MCP OAuth scope support + best‑practice authorization challenges #204
Checklist
npm run version. For example,use
npm run version:patchfor a patch version bump.environment variable or changing its default value.
Contributor Agreement
By submitting this pull request, I confirm that:
its Contribution Checklist.