[auth] Scenarios for scope selection - #36

Merged
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection
Nov 18, 2025
Merged

[auth] Scenarios for scope selection#36
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection

Conversation

@pcarleton

@pcarletonpcarleton commented Nov 18, 2025

Copy link
Copy Markdown
Member

Motivation and Context

fixes#32

How Has This Been Tested?

Breaking Changes

tests and negative tests

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
    • needs a typescript sdk change to pass
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

I did a few related refactors:

  • Inlined tests
  • allowed piping test output to jq
  • some fixes around how we handled WARNING, since it's a bunch of SHOULD's in this case

Comment threadsrc/runner/client.ts
const scenario = getScenario(scenarioName)!;

console.log(`Starting scenario: ${scenarioName}`);
console.error(`Starting scenario: ${scenarioName}`);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to make it possible to pipe verbose output to jq or jless

server.setRequestHandler(
CallToolRequestSchema,
async (request): Promise<CallToolResult> => {
if (request.params.name === 'test-tool') {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is so we can test an "auth only required on a tool call" scenario. It's via setRequestHandler because we're in the low level interface

private expectedScopes: string[] = []
) {}

registerToken(token: string, scopes: string[]) {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're keeping track of the scopes here as a convenience. we could also fetch them from the AS, and might want to switch to doing that in the future.

InlineClientRunner
} from './test_helpers/testClient.js';
import path from 'path';
import { runClient as goodClient } from '../../../../examples/clients/typescript/auth-test.js';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored the client tests to allow running them "inline" rather than spawning a subshell.

A subshell took a good 600ms, so the full auth suite took about 13s. With this, it's about 200ms for the full suite, so vitest watch is like a live refresh.

'examples/clients/typescript/auth-test.ts'
);
beforeAll(() => {
setLogLevel('error');

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without this, we get a bunch of annoying stdout lines littering the test output

// Verify that only the expected checks failed
const failures = nonInfoChecks.filter((c) => c.status === 'FAILURE');
const failures = nonInfoChecks.filter(
(c) => c.status === 'FAILURE' || c.status === 'WARNING'

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm counting warning as failure in this test because we want the examples to not have warnings

@pcarleton

Copy link
Copy Markdown
MemberAuthor

This is ready for review, but needs a typsecript change for tests to pass, will push that shortly.

@pcarleton

Copy link
Copy Markdown
MemberAuthor

typescript-sdk PR to have this pass:
modelcontextprotocol/typescript-sdk#1133

I think I'll comment out the broken ones for now since we'll need to wait for typescript release i think to get the fix. (unless we want to patch release?)

@pcarleton

Copy link
Copy Markdown
MemberAuthor

okay I skipped tests, we can unskip them later

* Broken client that only responds to 401, not 403.
* BUG: Ignores 403 responses which are used for step-up auth.
*/
export async function runClient(serverUrl: string): Promise<void> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more of a style nit: all these "broken" examples look very similar (identical?) except for the specific way in which they're "broken".

Could consider having a single implementation and having the specific way it's actually broken be a param to createBrokenHandler, a higher level function that creates the handle401broken.

 type ClientBehavior =
| { type: 'well-behaved' } // or just omit for default
| { type: 'ignore-resource-metadata' }
| { type: 'ignore-scope' }
| { type: 'partial-scopes'; scope: string }
| { type: 'ignore-403' };

Could make this less verbose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially the well-behaved client could also share the same implementation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we prefer explicitness over conciseness though for conformance tests, so definitely just a thought 🤔

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed on the repetitiveness, i've been resisting the urge to de-dupe too aggressively in order to let abstractions fall out from a few more iterations on tests before committing to one.

Comment threadpackage.json
"@types/express": "^5.0.3",
"@types/node": "^22.10.2",
"@typescript/native-preview": "^7.0.0-dev.20251030.1",
"cors": "^2.8.5",

@felixweinbergerfelixweinbergerNov 18, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is adding cors here intentional for this PR? Doesn't seem like we're importing this anywhere?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I ran npm link this started complaining, i think because the everything server uses it (even if we have a package.json in the server client repo).

I'd prefer to keep it to let the link flow be smoother even if it's a little odd.

@pcarleton
pcarleton merged commit 5c15113 into mainNov 18, 2025
4 of 7 checks passed
@pcarleton
pcarleton deleted the pcarleton/scopes-selection branch November 18, 2025 17:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client Auth: Scope Selection & Challenge Handling

2 participants

@pcarleton@felixweinberger
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[auth] Scenarios for scope selection - #36

Merged
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection
Nov 18, 2025
Merged

[auth] Scenarios for scope selection#36
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection

Conversation

@pcarleton

@pcarletonpcarleton commented Nov 18, 2025

Copy link
Copy Markdown
Member

Motivation and Context

fixes#32

How Has This Been Tested?

Breaking Changes

tests and negative tests

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
    • needs a typescript sdk change to pass
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

I did a few related refactors:

  • Inlined tests
  • allowed piping test output to jq
  • some fixes around how we handled WARNING, since it's a bunch of SHOULD's in this case

Comment threadsrc/runner/client.ts
const scenario = getScenario(scenarioName)!;

console.log(`Starting scenario: ${scenarioName}`);
console.error(`Starting scenario: ${scenarioName}`);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to make it possible to pipe verbose output to jq or jless

server.setRequestHandler(
CallToolRequestSchema,
async (request): Promise<CallToolResult> => {
if (request.params.name === 'test-tool') {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is so we can test an "auth only required on a tool call" scenario. It's via setRequestHandler because we're in the low level interface

private expectedScopes: string[] = []
) {}

registerToken(token: string, scopes: string[]) {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're keeping track of the scopes here as a convenience. we could also fetch them from the AS, and might want to switch to doing that in the future.

InlineClientRunner
} from './test_helpers/testClient.js';
import path from 'path';
import { runClient as goodClient } from '../../../../examples/clients/typescript/auth-test.js';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored the client tests to allow running them "inline" rather than spawning a subshell.

A subshell took a good 600ms, so the full auth suite took about 13s. With this, it's about 200ms for the full suite, so vitest watch is like a live refresh.

'examples/clients/typescript/auth-test.ts'
);
beforeAll(() => {
setLogLevel('error');

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without this, we get a bunch of annoying stdout lines littering the test output

// Verify that only the expected checks failed
const failures = nonInfoChecks.filter((c) => c.status === 'FAILURE');
const failures = nonInfoChecks.filter(
(c) => c.status === 'FAILURE' || c.status === 'WARNING'

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm counting warning as failure in this test because we want the examples to not have warnings

@pcarleton

Copy link
Copy Markdown
MemberAuthor

This is ready for review, but needs a typsecript change for tests to pass, will push that shortly.

@pcarleton

Copy link
Copy Markdown
MemberAuthor

typescript-sdk PR to have this pass:
modelcontextprotocol/typescript-sdk#1133

I think I'll comment out the broken ones for now since we'll need to wait for typescript release i think to get the fix. (unless we want to patch release?)

@pcarleton

Copy link
Copy Markdown
MemberAuthor

okay I skipped tests, we can unskip them later

* Broken client that only responds to 401, not 403.
* BUG: Ignores 403 responses which are used for step-up auth.
*/
export async function runClient(serverUrl: string): Promise<void> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more of a style nit: all these "broken" examples look very similar (identical?) except for the specific way in which they're "broken".

Could consider having a single implementation and having the specific way it's actually broken be a param to createBrokenHandler, a higher level function that creates the handle401broken.

 type ClientBehavior =
| { type: 'well-behaved' } // or just omit for default
| { type: 'ignore-resource-metadata' }
| { type: 'ignore-scope' }
| { type: 'partial-scopes'; scope: string }
| { type: 'ignore-403' };

Could make this less verbose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially the well-behaved client could also share the same implementation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we prefer explicitness over conciseness though for conformance tests, so definitely just a thought 🤔

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed on the repetitiveness, i've been resisting the urge to de-dupe too aggressively in order to let abstractions fall out from a few more iterations on tests before committing to one.

Comment threadpackage.json
"@types/express": "^5.0.3",
"@types/node": "^22.10.2",
"@typescript/native-preview": "^7.0.0-dev.20251030.1",
"cors": "^2.8.5",

@felixweinbergerfelixweinbergerNov 18, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is adding cors here intentional for this PR? Doesn't seem like we're importing this anywhere?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I ran npm link this started complaining, i think because the everything server uses it (even if we have a package.json in the server client repo).

I'd prefer to keep it to let the link flow be smoother even if it's a little odd.

@pcarleton
pcarleton merged commit 5c15113 into mainNov 18, 2025
4 of 7 checks passed
@pcarleton
pcarleton deleted the pcarleton/scopes-selection branch November 18, 2025 17:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client Auth: Scope Selection & Challenge Handling

2 participants

@pcarleton@felixweinberger
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[auth] Scenarios for scope selection - #36

Merged
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection
Nov 18, 2025
Merged

[auth] Scenarios for scope selection#36
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection

Conversation

@pcarleton

@pcarletonpcarleton commented Nov 18, 2025

Copy link
Copy Markdown
Member

Motivation and Context

fixes#32

How Has This Been Tested?

Breaking Changes

tests and negative tests

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
    • needs a typescript sdk change to pass
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

I did a few related refactors:

  • Inlined tests
  • allowed piping test output to jq
  • some fixes around how we handled WARNING, since it's a bunch of SHOULD's in this case

Comment threadsrc/runner/client.ts
const scenario = getScenario(scenarioName)!;

console.log(`Starting scenario: ${scenarioName}`);
console.error(`Starting scenario: ${scenarioName}`);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to make it possible to pipe verbose output to jq or jless

server.setRequestHandler(
CallToolRequestSchema,
async (request): Promise<CallToolResult> => {
if (request.params.name === 'test-tool') {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is so we can test an "auth only required on a tool call" scenario. It's via setRequestHandler because we're in the low level interface

private expectedScopes: string[] = []
) {}

registerToken(token: string, scopes: string[]) {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're keeping track of the scopes here as a convenience. we could also fetch them from the AS, and might want to switch to doing that in the future.

InlineClientRunner
} from './test_helpers/testClient.js';
import path from 'path';
import { runClient as goodClient } from '../../../../examples/clients/typescript/auth-test.js';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored the client tests to allow running them "inline" rather than spawning a subshell.

A subshell took a good 600ms, so the full auth suite took about 13s. With this, it's about 200ms for the full suite, so vitest watch is like a live refresh.

'examples/clients/typescript/auth-test.ts'
);
beforeAll(() => {
setLogLevel('error');

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without this, we get a bunch of annoying stdout lines littering the test output

// Verify that only the expected checks failed
const failures = nonInfoChecks.filter((c) => c.status === 'FAILURE');
const failures = nonInfoChecks.filter(
(c) => c.status === 'FAILURE' || c.status === 'WARNING'

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm counting warning as failure in this test because we want the examples to not have warnings

@pcarleton

Copy link
Copy Markdown
MemberAuthor

This is ready for review, but needs a typsecript change for tests to pass, will push that shortly.

@pcarleton

Copy link
Copy Markdown
MemberAuthor

typescript-sdk PR to have this pass:
modelcontextprotocol/typescript-sdk#1133

I think I'll comment out the broken ones for now since we'll need to wait for typescript release i think to get the fix. (unless we want to patch release?)

@pcarleton

Copy link
Copy Markdown
MemberAuthor

okay I skipped tests, we can unskip them later

* Broken client that only responds to 401, not 403.
* BUG: Ignores 403 responses which are used for step-up auth.
*/
export async function runClient(serverUrl: string): Promise<void> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more of a style nit: all these "broken" examples look very similar (identical?) except for the specific way in which they're "broken".

Could consider having a single implementation and having the specific way it's actually broken be a param to createBrokenHandler, a higher level function that creates the handle401broken.

 type ClientBehavior =
| { type: 'well-behaved' } // or just omit for default
| { type: 'ignore-resource-metadata' }
| { type: 'ignore-scope' }
| { type: 'partial-scopes'; scope: string }
| { type: 'ignore-403' };

Could make this less verbose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially the well-behaved client could also share the same implementation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we prefer explicitness over conciseness though for conformance tests, so definitely just a thought 🤔

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed on the repetitiveness, i've been resisting the urge to de-dupe too aggressively in order to let abstractions fall out from a few more iterations on tests before committing to one.

Comment threadpackage.json
"@types/express": "^5.0.3",
"@types/node": "^22.10.2",
"@typescript/native-preview": "^7.0.0-dev.20251030.1",
"cors": "^2.8.5",

@felixweinbergerfelixweinbergerNov 18, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is adding cors here intentional for this PR? Doesn't seem like we're importing this anywhere?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I ran npm link this started complaining, i think because the everything server uses it (even if we have a package.json in the server client repo).

I'd prefer to keep it to let the link flow be smoother even if it's a little odd.

@pcarleton
pcarleton merged commit 5c15113 into mainNov 18, 2025
4 of 7 checks passed
@pcarleton
pcarleton deleted the pcarleton/scopes-selection branch November 18, 2025 17:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client Auth: Scope Selection & Challenge Handling

2 participants

@pcarleton@felixweinberger
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[auth] Scenarios for scope selection - #36

Merged
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection
Nov 18, 2025
Merged

[auth] Scenarios for scope selection#36
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection

Conversation

@pcarleton

@pcarletonpcarleton commented Nov 18, 2025

Copy link
Copy Markdown
Member

Motivation and Context

fixes#32

How Has This Been Tested?

Breaking Changes

tests and negative tests

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
    • needs a typescript sdk change to pass
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

I did a few related refactors:

  • Inlined tests
  • allowed piping test output to jq
  • some fixes around how we handled WARNING, since it's a bunch of SHOULD's in this case

Comment threadsrc/runner/client.ts
const scenario = getScenario(scenarioName)!;

console.log(`Starting scenario: ${scenarioName}`);
console.error(`Starting scenario: ${scenarioName}`);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to make it possible to pipe verbose output to jq or jless

server.setRequestHandler(
CallToolRequestSchema,
async (request): Promise<CallToolResult> => {
if (request.params.name === 'test-tool') {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is so we can test an "auth only required on a tool call" scenario. It's via setRequestHandler because we're in the low level interface

private expectedScopes: string[] = []
) {}

registerToken(token: string, scopes: string[]) {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're keeping track of the scopes here as a convenience. we could also fetch them from the AS, and might want to switch to doing that in the future.

InlineClientRunner
} from './test_helpers/testClient.js';
import path from 'path';
import { runClient as goodClient } from '../../../../examples/clients/typescript/auth-test.js';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored the client tests to allow running them "inline" rather than spawning a subshell.

A subshell took a good 600ms, so the full auth suite took about 13s. With this, it's about 200ms for the full suite, so vitest watch is like a live refresh.

'examples/clients/typescript/auth-test.ts'
);
beforeAll(() => {
setLogLevel('error');

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without this, we get a bunch of annoying stdout lines littering the test output

// Verify that only the expected checks failed
const failures = nonInfoChecks.filter((c) => c.status === 'FAILURE');
const failures = nonInfoChecks.filter(
(c) => c.status === 'FAILURE' || c.status === 'WARNING'

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm counting warning as failure in this test because we want the examples to not have warnings

@pcarleton

Copy link
Copy Markdown
MemberAuthor

This is ready for review, but needs a typsecript change for tests to pass, will push that shortly.

@pcarleton

Copy link
Copy Markdown
MemberAuthor

typescript-sdk PR to have this pass:
modelcontextprotocol/typescript-sdk#1133

I think I'll comment out the broken ones for now since we'll need to wait for typescript release i think to get the fix. (unless we want to patch release?)

@pcarleton

Copy link
Copy Markdown
MemberAuthor

okay I skipped tests, we can unskip them later

* Broken client that only responds to 401, not 403.
* BUG: Ignores 403 responses which are used for step-up auth.
*/
export async function runClient(serverUrl: string): Promise<void> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more of a style nit: all these "broken" examples look very similar (identical?) except for the specific way in which they're "broken".

Could consider having a single implementation and having the specific way it's actually broken be a param to createBrokenHandler, a higher level function that creates the handle401broken.

 type ClientBehavior =
| { type: 'well-behaved' } // or just omit for default
| { type: 'ignore-resource-metadata' }
| { type: 'ignore-scope' }
| { type: 'partial-scopes'; scope: string }
| { type: 'ignore-403' };

Could make this less verbose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially the well-behaved client could also share the same implementation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we prefer explicitness over conciseness though for conformance tests, so definitely just a thought 🤔

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed on the repetitiveness, i've been resisting the urge to de-dupe too aggressively in order to let abstractions fall out from a few more iterations on tests before committing to one.

Comment threadpackage.json
"@types/express": "^5.0.3",
"@types/node": "^22.10.2",
"@typescript/native-preview": "^7.0.0-dev.20251030.1",
"cors": "^2.8.5",

@felixweinbergerfelixweinbergerNov 18, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is adding cors here intentional for this PR? Doesn't seem like we're importing this anywhere?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I ran npm link this started complaining, i think because the everything server uses it (even if we have a package.json in the server client repo).

I'd prefer to keep it to let the link flow be smoother even if it's a little odd.

@pcarleton
pcarleton merged commit 5c15113 into mainNov 18, 2025
4 of 7 checks passed
@pcarleton
pcarleton deleted the pcarleton/scopes-selection branch November 18, 2025 17:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client Auth: Scope Selection & Challenge Handling

2 participants

@pcarleton@felixweinberger
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[auth] Scenarios for scope selection - #36

Merged
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection
Nov 18, 2025
Merged

[auth] Scenarios for scope selection#36
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection

Conversation

@pcarleton

@pcarletonpcarleton commented Nov 18, 2025

Copy link
Copy Markdown
Member

Motivation and Context

fixes#32

How Has This Been Tested?

Breaking Changes

tests and negative tests

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
    • needs a typescript sdk change to pass
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

I did a few related refactors:

  • Inlined tests
  • allowed piping test output to jq
  • some fixes around how we handled WARNING, since it's a bunch of SHOULD's in this case

Comment threadsrc/runner/client.ts
const scenario = getScenario(scenarioName)!;

console.log(`Starting scenario: ${scenarioName}`);
console.error(`Starting scenario: ${scenarioName}`);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to make it possible to pipe verbose output to jq or jless

server.setRequestHandler(
CallToolRequestSchema,
async (request): Promise<CallToolResult> => {
if (request.params.name === 'test-tool') {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is so we can test an "auth only required on a tool call" scenario. It's via setRequestHandler because we're in the low level interface

private expectedScopes: string[] = []
) {}

registerToken(token: string, scopes: string[]) {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're keeping track of the scopes here as a convenience. we could also fetch them from the AS, and might want to switch to doing that in the future.

InlineClientRunner
} from './test_helpers/testClient.js';
import path from 'path';
import { runClient as goodClient } from '../../../../examples/clients/typescript/auth-test.js';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored the client tests to allow running them "inline" rather than spawning a subshell.

A subshell took a good 600ms, so the full auth suite took about 13s. With this, it's about 200ms for the full suite, so vitest watch is like a live refresh.

'examples/clients/typescript/auth-test.ts'
);
beforeAll(() => {
setLogLevel('error');

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without this, we get a bunch of annoying stdout lines littering the test output

// Verify that only the expected checks failed
const failures = nonInfoChecks.filter((c) => c.status === 'FAILURE');
const failures = nonInfoChecks.filter(
(c) => c.status === 'FAILURE' || c.status === 'WARNING'

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm counting warning as failure in this test because we want the examples to not have warnings

@pcarleton

Copy link
Copy Markdown
MemberAuthor

This is ready for review, but needs a typsecript change for tests to pass, will push that shortly.

@pcarleton

Copy link
Copy Markdown
MemberAuthor

typescript-sdk PR to have this pass:
modelcontextprotocol/typescript-sdk#1133

I think I'll comment out the broken ones for now since we'll need to wait for typescript release i think to get the fix. (unless we want to patch release?)

@pcarleton

Copy link
Copy Markdown
MemberAuthor

okay I skipped tests, we can unskip them later

* Broken client that only responds to 401, not 403.
* BUG: Ignores 403 responses which are used for step-up auth.
*/
export async function runClient(serverUrl: string): Promise<void> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more of a style nit: all these "broken" examples look very similar (identical?) except for the specific way in which they're "broken".

Could consider having a single implementation and having the specific way it's actually broken be a param to createBrokenHandler, a higher level function that creates the handle401broken.

 type ClientBehavior =
| { type: 'well-behaved' } // or just omit for default
| { type: 'ignore-resource-metadata' }
| { type: 'ignore-scope' }
| { type: 'partial-scopes'; scope: string }
| { type: 'ignore-403' };

Could make this less verbose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially the well-behaved client could also share the same implementation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we prefer explicitness over conciseness though for conformance tests, so definitely just a thought 🤔

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed on the repetitiveness, i've been resisting the urge to de-dupe too aggressively in order to let abstractions fall out from a few more iterations on tests before committing to one.

Comment threadpackage.json
"@types/express": "^5.0.3",
"@types/node": "^22.10.2",
"@typescript/native-preview": "^7.0.0-dev.20251030.1",
"cors": "^2.8.5",

@felixweinbergerfelixweinbergerNov 18, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is adding cors here intentional for this PR? Doesn't seem like we're importing this anywhere?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I ran npm link this started complaining, i think because the everything server uses it (even if we have a package.json in the server client repo).

I'd prefer to keep it to let the link flow be smoother even if it's a little odd.

@pcarleton
pcarleton merged commit 5c15113 into mainNov 18, 2025
4 of 7 checks passed
@pcarleton
pcarleton deleted the pcarleton/scopes-selection branch November 18, 2025 17:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client Auth: Scope Selection & Challenge Handling

2 participants

@pcarleton@felixweinberger
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[auth] Scenarios for scope selection - #36

Merged
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection
Nov 18, 2025
Merged

[auth] Scenarios for scope selection#36
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection

Conversation

@pcarleton

@pcarletonpcarleton commented Nov 18, 2025

Copy link
Copy Markdown
Member

Motivation and Context

fixes#32

How Has This Been Tested?

Breaking Changes

tests and negative tests

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
    • needs a typescript sdk change to pass
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

I did a few related refactors:

  • Inlined tests
  • allowed piping test output to jq
  • some fixes around how we handled WARNING, since it's a bunch of SHOULD's in this case

Comment threadsrc/runner/client.ts
const scenario = getScenario(scenarioName)!;

console.log(`Starting scenario: ${scenarioName}`);
console.error(`Starting scenario: ${scenarioName}`);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to make it possible to pipe verbose output to jq or jless

server.setRequestHandler(
CallToolRequestSchema,
async (request): Promise<CallToolResult> => {
if (request.params.name === 'test-tool') {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is so we can test an "auth only required on a tool call" scenario. It's via setRequestHandler because we're in the low level interface

private expectedScopes: string[] = []
) {}

registerToken(token: string, scopes: string[]) {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're keeping track of the scopes here as a convenience. we could also fetch them from the AS, and might want to switch to doing that in the future.

InlineClientRunner
} from './test_helpers/testClient.js';
import path from 'path';
import { runClient as goodClient } from '../../../../examples/clients/typescript/auth-test.js';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored the client tests to allow running them "inline" rather than spawning a subshell.

A subshell took a good 600ms, so the full auth suite took about 13s. With this, it's about 200ms for the full suite, so vitest watch is like a live refresh.

'examples/clients/typescript/auth-test.ts'
);
beforeAll(() => {
setLogLevel('error');

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without this, we get a bunch of annoying stdout lines littering the test output

// Verify that only the expected checks failed
const failures = nonInfoChecks.filter((c) => c.status === 'FAILURE');
const failures = nonInfoChecks.filter(
(c) => c.status === 'FAILURE' || c.status === 'WARNING'

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm counting warning as failure in this test because we want the examples to not have warnings

@pcarleton

Copy link
Copy Markdown
MemberAuthor

This is ready for review, but needs a typsecript change for tests to pass, will push that shortly.

@pcarleton

Copy link
Copy Markdown
MemberAuthor

typescript-sdk PR to have this pass:
modelcontextprotocol/typescript-sdk#1133

I think I'll comment out the broken ones for now since we'll need to wait for typescript release i think to get the fix. (unless we want to patch release?)

@pcarleton

Copy link
Copy Markdown
MemberAuthor

okay I skipped tests, we can unskip them later

* Broken client that only responds to 401, not 403.
* BUG: Ignores 403 responses which are used for step-up auth.
*/
export async function runClient(serverUrl: string): Promise<void> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more of a style nit: all these "broken" examples look very similar (identical?) except for the specific way in which they're "broken".

Could consider having a single implementation and having the specific way it's actually broken be a param to createBrokenHandler, a higher level function that creates the handle401broken.

 type ClientBehavior =
| { type: 'well-behaved' } // or just omit for default
| { type: 'ignore-resource-metadata' }
| { type: 'ignore-scope' }
| { type: 'partial-scopes'; scope: string }
| { type: 'ignore-403' };

Could make this less verbose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially the well-behaved client could also share the same implementation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we prefer explicitness over conciseness though for conformance tests, so definitely just a thought 🤔

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed on the repetitiveness, i've been resisting the urge to de-dupe too aggressively in order to let abstractions fall out from a few more iterations on tests before committing to one.

Comment threadpackage.json
"@types/express": "^5.0.3",
"@types/node": "^22.10.2",
"@typescript/native-preview": "^7.0.0-dev.20251030.1",
"cors": "^2.8.5",

@felixweinbergerfelixweinbergerNov 18, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is adding cors here intentional for this PR? Doesn't seem like we're importing this anywhere?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I ran npm link this started complaining, i think because the everything server uses it (even if we have a package.json in the server client repo).

I'd prefer to keep it to let the link flow be smoother even if it's a little odd.

@pcarleton
pcarleton merged commit 5c15113 into mainNov 18, 2025
4 of 7 checks passed
@pcarleton
pcarleton deleted the pcarleton/scopes-selection branch November 18, 2025 17:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client Auth: Scope Selection & Challenge Handling

2 participants

@pcarleton@felixweinberger
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[auth] Scenarios for scope selection - #36

Merged
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection
Nov 18, 2025
Merged

[auth] Scenarios for scope selection#36
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection

Conversation

@pcarleton

@pcarletonpcarleton commented Nov 18, 2025

Copy link
Copy Markdown
Member

Motivation and Context

fixes#32

How Has This Been Tested?

Breaking Changes

tests and negative tests

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
    • needs a typescript sdk change to pass
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

I did a few related refactors:

  • Inlined tests
  • allowed piping test output to jq
  • some fixes around how we handled WARNING, since it's a bunch of SHOULD's in this case

Comment threadsrc/runner/client.ts
const scenario = getScenario(scenarioName)!;

console.log(`Starting scenario: ${scenarioName}`);
console.error(`Starting scenario: ${scenarioName}`);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to make it possible to pipe verbose output to jq or jless

server.setRequestHandler(
CallToolRequestSchema,
async (request): Promise<CallToolResult> => {
if (request.params.name === 'test-tool') {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is so we can test an "auth only required on a tool call" scenario. It's via setRequestHandler because we're in the low level interface

private expectedScopes: string[] = []
) {}

registerToken(token: string, scopes: string[]) {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're keeping track of the scopes here as a convenience. we could also fetch them from the AS, and might want to switch to doing that in the future.

InlineClientRunner
} from './test_helpers/testClient.js';
import path from 'path';
import { runClient as goodClient } from '../../../../examples/clients/typescript/auth-test.js';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored the client tests to allow running them "inline" rather than spawning a subshell.

A subshell took a good 600ms, so the full auth suite took about 13s. With this, it's about 200ms for the full suite, so vitest watch is like a live refresh.

'examples/clients/typescript/auth-test.ts'
);
beforeAll(() => {
setLogLevel('error');

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without this, we get a bunch of annoying stdout lines littering the test output

// Verify that only the expected checks failed
const failures = nonInfoChecks.filter((c) => c.status === 'FAILURE');
const failures = nonInfoChecks.filter(
(c) => c.status === 'FAILURE' || c.status === 'WARNING'

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm counting warning as failure in this test because we want the examples to not have warnings

@pcarleton

Copy link
Copy Markdown
MemberAuthor

This is ready for review, but needs a typsecript change for tests to pass, will push that shortly.

@pcarleton

Copy link
Copy Markdown
MemberAuthor

typescript-sdk PR to have this pass:
modelcontextprotocol/typescript-sdk#1133

I think I'll comment out the broken ones for now since we'll need to wait for typescript release i think to get the fix. (unless we want to patch release?)

@pcarleton

Copy link
Copy Markdown
MemberAuthor

okay I skipped tests, we can unskip them later

* Broken client that only responds to 401, not 403.
* BUG: Ignores 403 responses which are used for step-up auth.
*/
export async function runClient(serverUrl: string): Promise<void> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more of a style nit: all these "broken" examples look very similar (identical?) except for the specific way in which they're "broken".

Could consider having a single implementation and having the specific way it's actually broken be a param to createBrokenHandler, a higher level function that creates the handle401broken.

 type ClientBehavior =
| { type: 'well-behaved' } // or just omit for default
| { type: 'ignore-resource-metadata' }
| { type: 'ignore-scope' }
| { type: 'partial-scopes'; scope: string }
| { type: 'ignore-403' };

Could make this less verbose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially the well-behaved client could also share the same implementation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we prefer explicitness over conciseness though for conformance tests, so definitely just a thought 🤔

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed on the repetitiveness, i've been resisting the urge to de-dupe too aggressively in order to let abstractions fall out from a few more iterations on tests before committing to one.

Comment threadpackage.json
"@types/express": "^5.0.3",
"@types/node": "^22.10.2",
"@typescript/native-preview": "^7.0.0-dev.20251030.1",
"cors": "^2.8.5",

@felixweinbergerfelixweinbergerNov 18, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is adding cors here intentional for this PR? Doesn't seem like we're importing this anywhere?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I ran npm link this started complaining, i think because the everything server uses it (even if we have a package.json in the server client repo).

I'd prefer to keep it to let the link flow be smoother even if it's a little odd.

@pcarleton
pcarleton merged commit 5c15113 into mainNov 18, 2025
4 of 7 checks passed
@pcarleton
pcarleton deleted the pcarleton/scopes-selection branch November 18, 2025 17:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client Auth: Scope Selection & Challenge Handling

2 participants

@pcarleton@felixweinberger
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[auth] Scenarios for scope selection - #36

Merged
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection
Nov 18, 2025
Merged

[auth] Scenarios for scope selection#36
pcarleton merged 17 commits into
mainfrom
pcarleton/scopes-selection

Conversation

@pcarleton

@pcarletonpcarleton commented Nov 18, 2025

Copy link
Copy Markdown
Member

Motivation and Context

fixes#32

How Has This Been Tested?

Breaking Changes

tests and negative tests

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
    • needs a typescript sdk change to pass
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

I did a few related refactors:

  • Inlined tests
  • allowed piping test output to jq
  • some fixes around how we handled WARNING, since it's a bunch of SHOULD's in this case

Comment threadsrc/runner/client.ts
const scenario = getScenario(scenarioName)!;

console.log(`Starting scenario: ${scenarioName}`);
console.error(`Starting scenario: ${scenarioName}`);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is to make it possible to pipe verbose output to jq or jless

server.setRequestHandler(
CallToolRequestSchema,
async (request): Promise<CallToolResult> => {
if (request.params.name === 'test-tool') {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is so we can test an "auth only required on a tool call" scenario. It's via setRequestHandler because we're in the low level interface

private expectedScopes: string[] = []
) {}

registerToken(token: string, scopes: string[]) {

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're keeping track of the scopes here as a convenience. we could also fetch them from the AS, and might want to switch to doing that in the future.

InlineClientRunner
} from './test_helpers/testClient.js';
import path from 'path';
import { runClient as goodClient } from '../../../../examples/clients/typescript/auth-test.js';

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored the client tests to allow running them "inline" rather than spawning a subshell.

A subshell took a good 600ms, so the full auth suite took about 13s. With this, it's about 200ms for the full suite, so vitest watch is like a live refresh.

'examples/clients/typescript/auth-test.ts'
);
beforeAll(() => {
setLogLevel('error');

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

without this, we get a bunch of annoying stdout lines littering the test output

// Verify that only the expected checks failed
const failures = nonInfoChecks.filter((c) => c.status === 'FAILURE');
const failures = nonInfoChecks.filter(
(c) => c.status === 'FAILURE' || c.status === 'WARNING'

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm counting warning as failure in this test because we want the examples to not have warnings

@pcarleton

Copy link
Copy Markdown
MemberAuthor

This is ready for review, but needs a typsecript change for tests to pass, will push that shortly.

@pcarleton

Copy link
Copy Markdown
MemberAuthor

typescript-sdk PR to have this pass:
modelcontextprotocol/typescript-sdk#1133

I think I'll comment out the broken ones for now since we'll need to wait for typescript release i think to get the fix. (unless we want to patch release?)

@pcarleton

Copy link
Copy Markdown
MemberAuthor

okay I skipped tests, we can unskip them later

* Broken client that only responds to 401, not 403.
* BUG: Ignores 403 responses which are used for step-up auth.
*/
export async function runClient(serverUrl: string): Promise<void> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more of a style nit: all these "broken" examples look very similar (identical?) except for the specific way in which they're "broken".

Could consider having a single implementation and having the specific way it's actually broken be a param to createBrokenHandler, a higher level function that creates the handle401broken.

 type ClientBehavior =
| { type: 'well-behaved' } // or just omit for default
| { type: 'ignore-resource-metadata' }
| { type: 'ignore-scope' }
| { type: 'partial-scopes'; scope: string }
| { type: 'ignore-403' };

Could make this less verbose.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially the well-behaved client could also share the same implementation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we prefer explicitness over conciseness though for conformance tests, so definitely just a thought 🤔

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed on the repetitiveness, i've been resisting the urge to de-dupe too aggressively in order to let abstractions fall out from a few more iterations on tests before committing to one.

Comment threadpackage.json
"@types/express": "^5.0.3",
"@types/node": "^22.10.2",
"@typescript/native-preview": "^7.0.0-dev.20251030.1",
"cors": "^2.8.5",

@felixweinbergerfelixweinbergerNov 18, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is adding cors here intentional for this PR? Doesn't seem like we're importing this anywhere?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when I ran npm link this started complaining, i think because the everything server uses it (even if we have a package.json in the server client repo).

I'd prefer to keep it to let the link flow be smoother even if it's a little odd.

@pcarleton
pcarleton merged commit 5c15113 into mainNov 18, 2025
4 of 7 checks passed
@pcarleton
pcarleton deleted the pcarleton/scopes-selection branch November 18, 2025 17:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Client Auth: Scope Selection & Challenge Handling

2 participants

@pcarleton@felixweinberger