Skip to content

Commit 8e5bec9

Browse files
committed
finished instructions
1 parent 4960b4b commit 8e5bec9

File tree

22 files changed

+334
-58
lines changed

22 files changed

+334
-58
lines changed

β€Žexercises/01.discovery/01.problem.cors/README.mdxβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ If we can't handle cross-origin requests properly, users will encounter frustrat
66

77
```ts
88
// Example: A user trying to access our MCP server from a different domain
9-
const response = await fetch('https://our-mcp-server.com/mcp', {
9+
const response = await fetch('https://our-mcp-server.example.com/mcp', {
1010
method: 'POST',
1111
headers: {
1212
'Content-Type': 'application/json',

β€Žexercises/01.discovery/02.problem.as/README.mdxβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ The problem is: how do we provide clients with the information they need to auth
77
```ts
88
// Example: A user trying to discover our OAuth server capabilities
99
const response = await fetch(
10-
'https://our-mcp-server.com/.well-known/oauth-authorization-server',
10+
'https://our-mcp-server.example.com/.well-known/oauth-authorization-server',
1111
)
1212
const metadata = await response.json()
1313
// metadata includes things like:

β€Žexercises/01.discovery/README.mdxβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ sequenceDiagram
4141
```ts
4242
// Discover resource server metadata
4343
const resourceMeta = await fetch(
44-
'https://our-mcp-server.com/.well-known/oauth-protected-resource',
44+
'https://our-mcp-server.example.com/.well-known/oauth-protected-resource',
4545
).then((r) => r.json())
4646

4747
// Find the authorization server URL

β€Žexercises/02.init/02.problem.params/README.mdxβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
For example, if a robot tries to fetch `/api/lemonade` without the right credentials, the response should include a realm and a resource_metadata parameter:
66

77
```
8-
WWW-Authenticate: Bearer realm="EpicMe", resource_metadata="https://lemonade-stand.com/.well-known/oauth-protected-resource/mcp"
8+
WWW-Authenticate: Bearer realm="EpicMe", resource_metadata="https://lemonade-stand.example.com/.well-known/oauth-protected-resource/mcp"
99
```
1010

1111
- **realm**: Identifies the protected area (like a journal or a lemonade stand) so clients know which resource needs credentials.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,40 @@
11
# Check Scopes
2+
3+
πŸ‘¨β€πŸ’Ό We're going to start adding more fine-grained control to what our MCP server can do by adding scopes. Our authorization server already supports scopes, and it makes no sense for the MCP server to show tools, resources, or prompts that the user doesn't have permission to use anyway.
4+
5+
This is where OAuth scopes come in. They give us fine-grained control over what authenticated users can access.
6+
7+
Let's look at an example of how this works in a music streaming app:
8+
9+
```ts
10+
// Define what scopes are available in our app
11+
const musicScopes = [
12+
'playlist:read', // View playlists
13+
'playlist:write', // Create/edit playlists
14+
'songs:read', // Search and play songs
15+
'profile:read', // View user profile
16+
] as const
17+
18+
// Check if user has permission to create playlists
19+
function hasScope(userScopes: Array<string>) {
20+
return userScopes.includes('playlist:write')
21+
}
22+
23+
// Only show "Create Playlist" if user has permission
24+
if (hasScope(authInfo.scopes)) {
25+
// show create playlist functionality
26+
}
27+
```
28+
29+
To make this work in our EpicMe journaling app, we need to create scope validation utilities. These will help us check what permissions users have and ensure they can only access the features they're authorized for.
30+
31+
<callout-info>
32+
πŸ§β€β™€οΈ I added `scopes` to the `whoami` tool so you can easily see what
33+
permissions are available when testing your implementation.
34+
</callout-info>
35+
36+
Once you have the scope validation utilities in place, you'll also need to add a convenient `hasScope` method to the `EpicMeMCP` class that makes it easy to check permissions throughout your app:
37+
38+
This approach means users will only see the tools and features they're actually authorized to use, creating a smooth and intuitive experience that matches their permission level.
39+
40+
πŸ“œ For more details on OAuth scopes and how they work in MCP servers, see the [OAuth 2.0 Scopes RFC](https://tools.ietf.org/html/rfc6749#section-3.3) and the [MCP Authentication Specification](https://modelcontextprotocol.io/specification/2025-06-18/server/auth).
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
# Check Scopes
2+
3+
πŸ‘¨β€πŸ’Ό Excellent work! We've successfully implemented scope validation utilities that give our EpicMe journaling app fine-grained control over what authenticated users can access. Now our MCP server can check if users have the required permissions before allowing them to perform sensitive operations like reading private entries or managing tags.
4+
5+
This improvement means users get proper access control based on their granted scopes, ensuring that personal journal data stays secure and only authorized actions are permitted. The scope validation utilities we built will be the foundation for all our permission checks going forward.
6+
7+
πŸ§β€β™€οΈ I'm expanding this scope validation system to cover more scenarios and adding better error handling for insufficient permissions. Feel free to try implementing that yourself, or just <NextDiffLink>check out my changes</NextDiffLink> if you'd like to see how it's done.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,60 @@
11
# Validate Sufficient Scope
2+
3+
πŸ‘¨β€πŸ’Ό Some users are authenticating with no scopes which results in having a valid token, but not enough permissions to actually do anything useful with the MCP server. It would be better if we could let them know up front that they don't have enough permissions and need a new token with the appropriate scopes.
4+
5+
To do this, you return a `403 Forbidden` response with the appropriate `WWW-Authenticate` header:
6+
7+
```ts
8+
// A music streaming app might check scopes like this
9+
const requiredScopes = ['music:read', 'playlists:write']
10+
const userScopes = ['music:read'] // User only has read access
11+
12+
const hasRequiredScopes = requiredScopes.every((scope) =>
13+
userScopes.includes(scope),
14+
)
15+
16+
if (!hasRequiredScopes) {
17+
return new Response('Forbidden', {
18+
status: 403,
19+
headers: {
20+
'WWW-Authenticate': [
21+
`Bearer realm="MusicApp"`,
22+
`error="insufficient_scope"`,
23+
`scopes="${requiredScopes.join(' ')}"`,
24+
].join(', '),
25+
},
26+
})
27+
}
28+
```
29+
30+
The `WWW-Authenticate` header includes:
31+
32+
- The realm (`MusicApp`)
33+
- The error type (`insufficient_scope`)
34+
- The required scopes (`music:read playlists:write`)
35+
36+
However, the `scopes` auth param is tricky for us because there's actually a list of valid scope combinations that are allowed. The user could have one of several scopes that would be enough to use the MCP server. There's not an established pattern for handling this case, so we'll skip the `scopes` auth param and instead include a `error_description` that explains what scope combinations are valid.
37+
38+
```mermaid
39+
sequenceDiagram
40+
Client->>MCP_Server: Request with valid token
41+
MCP_Server->>Auth_Server: Introspect token
42+
Auth_Server-->>MCP_Server: Token info + scopes
43+
MCP_Server->>MCP_Server: Check hasSufficientScope()
44+
alt Has sufficient scope
45+
MCP_Server-->>Client: Success - MCP server access granted
46+
else Insufficient scope
47+
MCP_Server-->>Client: 403 Forbidden with scope requirements
48+
end
49+
```
50+
51+
<callout-muted>
52+
πŸ“œ For more details on OAuth scope validation and error handling, see the
53+
[OAuth 2.0 Authorization Framework
54+
RFC](https://tools.ietf.org/html/rfc6749#section-3.3) and [OAuth 2.0 Bearer
55+
Token Usage RFC](https://tools.ietf.org/html/rfc6750#section-3.1).
56+
</callout-muted>
57+
58+
πŸ§β€β™€οΈ I created a `minimalValidScopeCombinations` array and a `hasSufficientScope` function for you to use. It's just hard to describe what you should do, but simple once you see it. Feel free to <PrevDiffLink>check out my changes</PrevDiffLink> if you want.
59+
60+
Now, let's implement the scope validation logic to ensure only properly authorized clients can access the EpicMe MCP server!

β€Žexercises/05.scopes/02.problem.validate-sufficient-scope/src/auth.tsβ€Ž

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,8 @@ export function hasSufficientScope(authInfo: AuthInfo) {
8989
// 🐨 create a handleInsufficientScope function that returns a 403 response with
9090
// the appropriate WWW-Authenticate header.
9191
// The header should be similar to the handleUnauthorized one below. It needs
92-
// the following auth params: error, error_description, and error_uri
92+
// the following auth params: error and error_description
9393
// πŸ’° use the minimalValidScopeCombinations array to create the error_description to explain the valid combinations of scopes
94-
// πŸ’° use the url.toString() to create the error_uri (this is the same as the handleUnauthorized one below)
9594

9695
export function handleUnauthorized(request: Request) {
9796
const hasAuthHeader = request.headers.has('authorization')

β€Žexercises/05.scopes/02.problem.validate-sufficient-scope/src/index.tsβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export default {
107107
if (!authInfo) return handleUnauthorized(request)
108108

109109
// 🐨 check whether the authInfo includes all the required scopes
110-
// 🐨 if it doesn't, call and return the result of handleInsufficientScope(request)
110+
// 🐨 if it doesn't, call and return the result of handleInsufficientScope()
111111

112112
const mcp = EpicMeMCP.serve('/mcp', {
113113
binding: 'EPIC_ME_MCP_OBJECT',
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
# Validate Sufficient Scope
2+
3+
πŸ‘¨β€πŸ’Ό Excellent work! Now users won't be able to access the MCP server if they don't have useful permissions.

0 commit comments

Comments
Β (0)