apple muncy commited on
Commit
95fad9c
·
1 Parent(s): b3cd821

Signed-off-by: apple muncy <apple@llama-3.local>

Browse files
Files changed (5) hide show
  1. Dockerfile +17 -0
  2. README.md +3 -2
  3. requirements.txt +1 -0
  4. server.py +180 -0
  5. token_verifier.py +105 -0
Dockerfile ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11
2
+
3
+ RUN useradd -m -u 1000 user
4
+ USER user
5
+ ENV HOME=/home/user \
6
+ PATH="/home/user/.local/bin:$PATH"
7
+ RUN mkdir -p /home/user/app
8
+ WORKDIR /home/user/app
9
+
10
+ COPY --chown=user ./requirements.txt requirements.txt
11
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
12
+
13
+ COPY --chown=user . /home/user/app
14
+
15
+ EXPOSE 7860
16
+
17
+ ENTRYPOINT ["python", "server.py"]
README.md CHANGED
@@ -1,12 +1,13 @@
1
  ---
2
- title: Rs
3
  emoji: 😻
4
  colorFrom: indigo
5
  colorTo: gray
6
  sdk: docker
7
- pinned: false
8
  license: apache-2.0
9
  short_description: 'Mcp Resource Server with OAuth '
 
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: MCP Resource Server
3
  emoji: 😻
4
  colorFrom: indigo
5
  colorTo: gray
6
  sdk: docker
7
+ pinned: true
8
  license: apache-2.0
9
  short_description: 'Mcp Resource Server with OAuth '
10
+ app_port: 7860
11
  ---
12
 
13
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ mcp==1.11.0
server.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Resource Server with Token Introspection.
3
+
4
+ This server validates tokens via Authorization Server introspection and serves MCP resources.
5
+ Demonstrates RFC 9728 Protected Resource Metadata for AS/RS separation.
6
+
7
+ NOTE: this is a simplified example for demonstration purposes.
8
+ This is not a production-ready implementation.
9
+ """
10
+
11
+ import datetime
12
+ import logging
13
+ from typing import Any, Literal
14
+ import os
15
+ import click
16
+ from pydantic import AnyHttpUrl
17
+ from pydantic_settings import BaseSettings, SettingsConfigDict
18
+
19
+ from mcp.server.auth.settings import AuthSettings
20
+ from mcp.server.fastmcp.server import FastMCP
21
+
22
+ from token_verifier import IntrospectionTokenVerifier
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class ResourceServerSettings(BaseSettings):
28
+ """Settings for the MCP Resource Server."""
29
+
30
+ model_config = SettingsConfigDict(env_prefix="MCP_RESOURCE_")
31
+
32
+ # Server settings
33
+ host: str = "0.0.0.0"
34
+ port: int = 7860
35
+ server_url: AnyHttpUrl = AnyHttpUrl("http://applemuncy-rs.hf,space")
36
+
37
+ # Authorization Server settings
38
+ auth_server_url: AnyHttpUrl = AnyHttpUrl("https://applemuncy-as.hf.space")
39
+ auth_server_introspection_endpoint: str = "https://applemuncy-as.hf.space/introspect"
40
+ # No user endpoint needed - we get user data from token introspection
41
+
42
+ # MCP settings
43
+ mcp_scope: str = "user"
44
+
45
+ # RFC 8707 resource validation
46
+ oauth_strict: bool = True
47
+
48
+ def __init__(self, **data):
49
+ """Initialize settings with values from environment variables."""
50
+ super().__init__(**data)
51
+
52
+
53
+ def create_resource_server(settings: ResourceServerSettings) -> FastMCP:
54
+ """
55
+ Create MCP Resource Server with token introspection.
56
+
57
+ This server:
58
+ 1. Provides protected resource metadata (RFC 9728)
59
+ 2. Validates tokens via Authorization Server introspection
60
+ 3. Serves MCP tools and resources
61
+ """
62
+ # Create token verifier for introspection with RFC 8707 resource validation
63
+ token_verifier = IntrospectionTokenVerifier(
64
+ introspection_endpoint=settings.auth_server_introspection_endpoint,
65
+ server_url=str(settings.server_url),
66
+ validate_resource=settings.oauth_strict, # Only validate when --oauth-strict is set
67
+ )
68
+
69
+ # Create FastMCP server as a Resource Server
70
+ app = FastMCP(
71
+ name="MCP Resource Server",
72
+ instructions="Resource Server that validates tokens via Authorization Server introspection",
73
+ host=settings.host,
74
+ port=settings.port,
75
+ debug=True,
76
+ # Auth configuration for RS mode
77
+ token_verifier=token_verifier,
78
+ auth=AuthSettings(
79
+ issuer_url=settings.auth_server_url,
80
+ required_scopes=[settings.mcp_scope],
81
+ resource_server_url=settings.server_url,
82
+ ),
83
+ )
84
+ @app.tool()
85
+ async def ls() -> list[str]:
86
+ """
87
+ list cerrent directory.
88
+
89
+ This tool demonstarates the user can get a listing of the current directory
90
+ """
91
+ listing = os.listdir('.')
92
+
93
+ return listing
94
+
95
+
96
+ @app.tool()
97
+ async def get_time() -> dict[str, Any]:
98
+ """
99
+ Get the current server time.
100
+
101
+ This tool demonstrates that system information can be protected
102
+ by OAuth authentication. User must be authenticated to access it.
103
+ """
104
+
105
+ now = datetime.datetime.now()
106
+
107
+ return {
108
+ "current_time": now.isoformat(),
109
+ "timezone": "UTC", # Simplified for demo
110
+ "timestamp": now.timestamp(),
111
+ "formatted": now.strftime("%Y-%m-%d %H:%M:%S"),
112
+ }
113
+
114
+ return app
115
+
116
+
117
+ @click.command()
118
+ @click.option("--port", default=8001, help="Port to listen on")
119
+ @click.option("--auth-server", default="http://applemuncy-as.hf.space", help="Authorization Server URL")
120
+ @click.option(
121
+ "--transport",
122
+ default="streamable-http",
123
+ type=click.Choice(["sse", "streamable-http"]),
124
+ help="Transport protocol to use ('sse' or 'streamable-http')",
125
+ )
126
+ @click.option(
127
+ "--oauth-strict",
128
+ is_flag=True,
129
+ help="Enable RFC 8707 resource validation",
130
+ )
131
+ def main(port: int, auth_server: str, transport: Literal["sse", "streamable-http"], oauth_strict: bool) -> int:
132
+ """
133
+ Run the MCP Resource Server.
134
+
135
+ This server:
136
+ - Provides RFC 9728 Protected Resource Metadata
137
+ - Validates tokens via Authorization Server introspection
138
+ - Serves MCP tools requiring authentication
139
+
140
+ Must be used with a running Authorization Server.
141
+ """
142
+ logging.basicConfig(level=logging.INFO)
143
+
144
+ try:
145
+ # Parse auth server URL
146
+ auth_server_url = AnyHttpUrl(auth_server)
147
+
148
+ # Create settings
149
+ host = "applemuncy-rs.hf.space"
150
+ server_url = f"http://{host}:{port}"
151
+ settings = ResourceServerSettings(
152
+ host="0.0.0.0",
153
+ port=port,
154
+ server_url=AnyHttpUrl(server_url),
155
+ auth_server_url=auth_server_url,
156
+ auth_server_introspection_endpoint=f"{auth_server}/introspect",
157
+ oauth_strict=oauth_strict,
158
+ )
159
+ except ValueError as e:
160
+ logger.error(f"Configuration error: {e}")
161
+ logger.error("Make sure to provide a valid Authorization Server URL")
162
+ return 1
163
+
164
+ try:
165
+ mcp_server = create_resource_server(settings)
166
+
167
+ logger.info(f"🚀 MCP Resource Server running on {settings.server_url}")
168
+ logger.info(f"🔑 Using Authorization Server: {settings.auth_server_url}")
169
+
170
+ # Run the server - this should block and keep running
171
+ mcp_server.run(transport=transport)
172
+ logger.info("Server stopped")
173
+ return 0
174
+ except Exception:
175
+ logger.exception("Server error")
176
+ return 1
177
+
178
+
179
+ if __name__ == "__main__":
180
+ main() # type: ignore[call-arg]
token_verifier.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Example token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
2
+
3
+ import logging
4
+
5
+ from mcp.server.auth.provider import AccessToken, TokenVerifier
6
+ from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class IntrospectionTokenVerifier(TokenVerifier):
12
+ """Example token verifier that uses OAuth 2.0 Token Introspection (RFC 7662).
13
+
14
+ This is a simple example implementation for demonstration purposes.
15
+ Production implementations should consider:
16
+ - Connection pooling and reuse
17
+ - More sophisticated error handling
18
+ - Rate limiting and retry logic
19
+ - Comprehensive configuration options
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ introspection_endpoint: str,
25
+ server_url: str,
26
+ validate_resource: bool = False,
27
+ ):
28
+ self.introspection_endpoint = introspection_endpoint
29
+ self.server_url = server_url
30
+ self.validate_resource = validate_resource
31
+ self.resource_url = resource_url_from_server_url(server_url)
32
+
33
+ async def verify_token(self, token: str) -> AccessToken | None:
34
+ """Verify token via introspection endpoint."""
35
+ import httpx
36
+
37
+ # Validate URL to prevent SSRF attacks
38
+ if not self.introspection_endpoint.startswith(("https://", "http://localhost", "http://127.0.0.1")):
39
+ logger.warning(f"Rejecting introspection endpoint with unsafe scheme: {self.introspection_endpoint}")
40
+ return None
41
+
42
+ # Configure secure HTTP client
43
+ timeout = httpx.Timeout(10.0, connect=5.0)
44
+ limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)
45
+
46
+ async with httpx.AsyncClient(
47
+ timeout=timeout,
48
+ limits=limits,
49
+ verify=True, # Enforce SSL verification
50
+ ) as client:
51
+ try:
52
+ response = await client.post(
53
+ self.introspection_endpoint,
54
+ data={"token": token},
55
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
56
+ )
57
+
58
+ if response.status_code != 200:
59
+ logger.debug(f"Token introspection returned status {response.status_code}")
60
+ return None
61
+
62
+ data = response.json()
63
+ if not data.get("active", False):
64
+ return None
65
+
66
+ # RFC 8707 resource validation (only when --oauth-strict is set)
67
+ if self.validate_resource and not self._validate_resource(data):
68
+ logger.warning(f"Token resource validation failed. Expected: {self.resource_url}")
69
+ return None
70
+
71
+ return AccessToken(
72
+ token=token,
73
+ client_id=data.get("client_id", "unknown"),
74
+ scopes=data.get("scope", "").split() if data.get("scope") else [],
75
+ expires_at=data.get("exp"),
76
+ resource=data.get("aud"), # Include resource in token
77
+ )
78
+ except Exception as e:
79
+ logger.warning(f"Token introspection failed: {e}")
80
+ return None
81
+
82
+ def _validate_resource(self, token_data: dict) -> bool:
83
+ """Validate token was issued for this resource server."""
84
+ if not self.server_url or not self.resource_url:
85
+ return False # Fail if strict validation requested but URLs missing
86
+
87
+ # Check 'aud' claim first (standard JWT audience)
88
+ aud = token_data.get("aud")
89
+ if isinstance(aud, list):
90
+ for audience in aud:
91
+ if self._is_valid_resource(audience):
92
+ return True
93
+ return False
94
+ elif aud:
95
+ return self._is_valid_resource(aud)
96
+
97
+ # No resource binding - invalid per RFC 8707
98
+ return False
99
+
100
+ def _is_valid_resource(self, resource: str) -> bool:
101
+ """Check if resource matches this server using hierarchical matching."""
102
+ if not self.resource_url:
103
+ return False
104
+
105
+ return check_resource_allowed(requested_resource=self.resource_url, configured_resource=resource)