Skip to content

Commit c62ccfc

Browse files
committed
refactor(core): move mergeScopes to shared authUtils
1 parent 2eb5a7f commit c62ccfc

4 files changed

Lines changed: 52 additions & 32 deletions

File tree

packages/client/src/client/sse.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
11
import type { FetchLike, JSONRPCMessage, Transport } from '@modelcontextprotocol/core';
2-
import { createFetchWithInit, JSONRPCMessageSchema, normalizeHeaders, SdkError, SdkErrorCode } from '@modelcontextprotocol/core';
2+
import {
3+
createFetchWithInit,
4+
JSONRPCMessageSchema,
5+
mergeScopes,
6+
normalizeHeaders,
7+
SdkError,
8+
SdkErrorCode
9+
} from '@modelcontextprotocol/core';
310
import type { ErrorEvent, EventSourceInit } from 'eventsource';
411
import { EventSource } from 'eventsource';
512

613
import type { AuthProvider, OAuthClientProvider } from './auth.js';
714
import { adaptOAuthProvider, auth, extractWWWAuthenticateParams, isOAuthClientProvider, UnauthorizedError } from './auth.js';
815

9-
/**
10-
* Merges two space-separated OAuth scope strings into a deduplicated union.
11-
* Returns undefined when the resulting set is empty.
12-
* Preserves insertion order of first occurrence for determinism.
13-
*/
14-
function mergeScopes(existing: string | undefined, incoming: string | undefined): string | undefined {
15-
const existingTokens = existing?.split(/\s+/).filter(Boolean) ?? [];
16-
const incomingTokens = incoming?.split(/\s+/).filter(Boolean) ?? [];
17-
const merged = new Set<string>([...existingTokens, ...incomingTokens]);
18-
if (merged.size === 0) {
19-
return undefined;
20-
}
21-
return [...merged].join(' ');
22-
}
23-
2416
export class SseError extends Error {
2517
constructor(
2618
public readonly code: number | undefined,

packages/client/src/client/streamableHttp.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
isJSONRPCRequest,
88
isJSONRPCResultResponse,
99
JSONRPCMessageSchema,
10+
mergeScopes,
1011
normalizeHeaders,
1112
SdkError,
1213
SdkErrorCode
@@ -16,21 +17,6 @@ import { EventSourceParserStream } from 'eventsource-parser/stream';
1617
import type { AuthProvider, OAuthClientProvider } from './auth.js';
1718
import { adaptOAuthProvider, auth, extractWWWAuthenticateParams, isOAuthClientProvider, UnauthorizedError } from './auth.js';
1819

19-
/**
20-
* Merges two space-separated OAuth scope strings into a deduplicated union.
21-
* Returns undefined when the resulting set is empty.
22-
* Preserves insertion order of first occurrence for determinism.
23-
*/
24-
function mergeScopes(existing: string | undefined, incoming: string | undefined): string | undefined {
25-
const existingTokens = existing?.split(/\s+/).filter(Boolean) ?? [];
26-
const incomingTokens = incoming?.split(/\s+/).filter(Boolean) ?? [];
27-
const merged = new Set<string>([...existingTokens, ...incomingTokens]);
28-
if (merged.size === 0) {
29-
return undefined;
30-
}
31-
return [...merged].join(' ');
32-
}
33-
3420
// Default reconnection options for StreamableHTTP connections
3521
const DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS: StreamableHTTPReconnectionOptions = {
3622
initialReconnectionDelay: 1000,

packages/core/src/shared/authUtils.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,18 @@ export function checkResourceAllowed({
5555

5656
return requestedPath.startsWith(configuredPath);
5757
}
58+
59+
/**
60+
* Merges two space-separated OAuth scope strings into a deduplicated union.
61+
* Returns undefined when the resulting set is empty.
62+
* Preserves insertion order of first occurrence for determinism.
63+
*/
64+
export function mergeScopes(existing: string | undefined, incoming: string | undefined): string | undefined {
65+
const existingTokens = existing?.split(/\s+/).filter(Boolean) ?? [];
66+
const incomingTokens = incoming?.split(/\s+/).filter(Boolean) ?? [];
67+
const merged = new Set<string>([...existingTokens, ...incomingTokens]);
68+
if (merged.size === 0) {
69+
return undefined;
70+
}
71+
return [...merged].join(' ');
72+
}

packages/core/test/shared/authUtils.test.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { checkResourceAllowed, resourceUrlFromServerUrl } from '../../src/shared/authUtils.js';
1+
import { checkResourceAllowed, mergeScopes, resourceUrlFromServerUrl } from '../../src/shared/authUtils.js';
22

33
describe('auth-utils', () => {
44
describe('resourceUrlFromServerUrl', () => {
@@ -87,4 +87,31 @@ describe('auth-utils', () => {
8787
).toBe(false);
8888
});
8989
});
90+
91+
describe('mergeScopes', () => {
92+
it('should return undefined when both inputs are undefined', () => {
93+
expect(mergeScopes(undefined, undefined)).toBeUndefined();
94+
});
95+
96+
it('should return existing when incoming is undefined', () => {
97+
expect(mergeScopes('read write', undefined)).toBe('read write');
98+
});
99+
100+
it('should return incoming when existing is undefined', () => {
101+
expect(mergeScopes(undefined, 'read write')).toBe('read write');
102+
});
103+
104+
it('should merge and deduplicate scopes', () => {
105+
expect(mergeScopes('read write', 'write execute')).toBe('read write execute');
106+
});
107+
108+
it('should return undefined for empty strings', () => {
109+
expect(mergeScopes('', '')).toBeUndefined();
110+
expect(mergeScopes('', undefined)).toBeUndefined();
111+
});
112+
113+
it('should handle multiple whitespace separators', () => {
114+
expect(mergeScopes('read write', 'write\texecute')).toBe('read write execute');
115+
});
116+
});
90117
});

0 commit comments

Comments
 (0)