manmo12 commited on
Commit
b3efdb3
·
1 Parent(s): c2ea4ad
Files changed (2) hide show
  1. echo_server.py +138 -18
  2. requirements.txt +63 -6
echo_server.py CHANGED
@@ -1,27 +1,147 @@
 
1
  from fastmcp import FastMCP
2
- from fastmcp.server.auth import RemoteAuthProvider
3
- from fastmcp.server.auth.providers.jwt import JWTVerifier
4
- from pydantic import AnyHttpUrl
5
  import os
 
6
 
7
- # For OAuth 2.0 with Twitter, we'll use JWT verification
8
- # Twitter provides JWKS for token verification
9
- token_verifier = JWTVerifier(
10
- jwks_uri="https://api.twitter.com/2/oauth2/jwks",
11
- issuer="https://api.twitter.com",
12
- audience=os.getenv("TWITTER_CLIENT_ID", "your_twitter_client_id"),
 
 
 
 
13
  )
14
 
15
- # Create the remote auth provider
16
- auth = RemoteAuthProvider(
17
- token_verifier=token_verifier,
18
- authorization_servers=[AnyHttpUrl("https://twitter.com")],
19
- base_url=os.getenv("BASE_URL", "https://myuniverse01-deploy1.hf.space/echo"),
 
 
 
 
 
 
 
20
  )
21
 
22
- mcp = FastMCP(name="EchoServer", stateless_http=True, auth=auth)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
 
 
 
 
 
24
 
25
- @mcp.tool(description="A simple echo tool")
26
- def echo(message: str) -> str:
27
- return f"Echo: {message}"
 
1
+ from fastapi import FastAPI, Request, HTTPException
2
  from fastmcp import FastMCP
3
+ from authlib.integrations.starlette_client import OAuth
4
+ from starlette.middleware.sessions import SessionMiddleware
 
5
  import os
6
+ from typing import Dict, Any, Optional
7
 
8
+ # Validation des variables d'environnement
9
+ required_env_vars = ["CLIENT_ID", "CLIENT_SECRET"]
10
+ missing_vars = [var for var in required_env_vars if not os.getenv(var)]
11
+ if missing_vars:
12
+ raise ValueError(f"Variables d'environnement manquantes: {', '.join(missing_vars)}")
13
+
14
+ app = FastAPI()
15
+ app.add_middleware(
16
+ SessionMiddleware,
17
+ secret_key="CHANGE_ME"
18
  )
19
 
20
+ # --- Config OAuth Twitter ---
21
+ oauth = OAuth()
22
+ oauth.register(
23
+ name='twitter',
24
+ client_id=os.getenv("CLIENT_ID"),
25
+ client_secret=os.getenv("CLIENT_SECRET"),
26
+ authorize_url="https://twitter.com/i/oauth2/authorize",
27
+ access_token_url="https://api.twitter.com/2/oauth2/token",
28
+ client_kwargs={
29
+ "scope": "tweet.read users.read offline.access",
30
+ "token_endpoint_auth_method": "client_secret_post"
31
+ }
32
  )
33
 
34
+ # --- Fonction d'authentification pour MCP ---
35
+ async def mcp_auth(request: Request) -> Optional[Dict[str, Any]]:
36
+ """
37
+ Fonction d'authentification pour FastMCP
38
+ Retourne les données utilisateur si authentifié, None sinon
39
+ """
40
+ user = request.session.get('user')
41
+ if not user:
42
+ return None
43
+ return user
44
+
45
+ # --- FastMCP Server avec auth intégrée ---
46
+ mcp = FastMCP(
47
+ name="TwitterMCP",
48
+ stateless_http=True,
49
+ auth=mcp_auth
50
+ )
51
+
52
+ @mcp.tool()
53
+ async def hello() -> str:
54
+ """Fonction de test MCP"""
55
+ return "Hello depuis FastMCP 🎉"
56
+
57
+ @mcp.tool()
58
+ async def get_current_user(request: Request) -> Dict[str, Any]:
59
+ """Retourne les informations de l'utilisateur authentifié"""
60
+ user = request.session.get('user', {})
61
+ return {
62
+ "username": user.get('username'),
63
+ "id": user.get('id'),
64
+ "name": user.get('name')
65
+ }
66
+
67
+ @mcp.tool()
68
+ async def tweet_something(request: Request, message: str) -> Dict[str, str]:
69
+ """
70
+ Exemple d'outil qui pourrait tweeter (nécessiterait les bonnes permissions)
71
+ """
72
+ user = request.session.get('user', {})
73
+ # Ici on pourrait utiliser le token stocké pour poster un tweet
74
+ return {
75
+ "status": "success",
76
+ "message": f"Tweet simulé de @{user.get('username', 'unknown')}: {message}"
77
+ }
78
+
79
+ # --- Routes OAuth ---
80
+ @app.get("/login")
81
+ async def login(request: Request):
82
+ """Initie le processus d'authentification Twitter"""
83
+ try:
84
+ redirect_uri = request.url_for("auth_callback")
85
+ return await oauth.twitter.authorize_redirect(request, redirect_uri)
86
+ except Exception as e:
87
+ raise HTTPException(status_code=500, detail=f"Erreur lors de l'initialisation OAuth: {str(e)}")
88
+
89
+ @app.get("/callback")
90
+ async def auth_callback(request: Request) -> Dict[str, str]:
91
+ """Gère le callback OAuth et stocke les infos utilisateur"""
92
+ try:
93
+ token = await oauth.twitter.authorize_access_token(request)
94
+
95
+ # Récupère les infos de l'utilisateur
96
+ user_req = await oauth.twitter.get(
97
+ "https://api.twitter.com/2/users/me",
98
+ token=token
99
+ )
100
+
101
+ if user_req.status_code != 200:
102
+ raise HTTPException(
103
+ status_code=400,
104
+ detail="Impossible de récupérer les informations utilisateur"
105
+ )
106
+
107
+ userinfo = user_req.json().get('data', {})
108
+
109
+ if not userinfo:
110
+ raise HTTPException(
111
+ status_code=400,
112
+ detail="Données utilisateur invalides"
113
+ )
114
+
115
+ # Stocke les infos utilisateur et le token en session
116
+ request.session['user'] = userinfo
117
+ request.session['token'] = token
118
+
119
+ return {"message": f"Connecté en tant que @{userinfo.get('username', 'utilisateur')}"}
120
+
121
+ except HTTPException:
122
+ raise
123
+ except Exception as e:
124
+ raise HTTPException(status_code=500, detail=f"Erreur lors de l'authentification: {str(e)}")
125
+
126
+ @app.get("/logout")
127
+ async def logout(request: Request):
128
+ """Déconnecte l'utilisateur"""
129
+ request.session.clear()
130
+ return {"message": "Déconnecté avec succès"}
131
+
132
+ @app.get("/profile")
133
+ async def get_profile(request: Request):
134
+ """Retourne le profil de l'utilisateur connecté"""
135
+ user = request.session.get('user')
136
+ if not user:
137
+ raise HTTPException(status_code=401, detail="Non authentifié")
138
+ return {"user": user}
139
 
140
+ # --- Route de santé ---
141
+ @app.get("/health")
142
+ async def health_check():
143
+ """Vérification de l'état de l'application"""
144
+ return {"status": "ok", "message": "Service opérationnel"}
145
 
146
+ # --- Intégration FastMCP dans FastAPI ---
147
+ app.mount("/mcp", mcp.app)
 
requirements.txt CHANGED
@@ -1,6 +1,63 @@
1
- fastmcp>=2.12.2
2
- httpx>=0.27.0
3
- pydantic>=2.7.0
4
- selectolax>=0.3.15
5
- fastapi
6
- jinja2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ annotated-types==0.7.0
2
+ anyio==4.10.0
3
+ attrs==25.3.0
4
+ Authlib==1.6.3
5
+ certifi==2025.8.3
6
+ cffi==2.0.0
7
+ charset-normalizer==3.4.3
8
+ click==8.2.1
9
+ cryptography==45.0.7
10
+ cyclopts==3.24.0
11
+ dnspython==2.8.0
12
+ docstring_parser==0.17.0
13
+ docutils==0.22
14
+ email-validator==2.3.0
15
+ exceptiongroup==1.3.0
16
+ fastapi==0.116.1
17
+ fastmcp==2.12.3
18
+ h11==0.16.0
19
+ httpcore==1.0.9
20
+ httpx==0.28.1
21
+ httpx-sse==0.4.1
22
+ idna==3.10
23
+ isodate==0.7.2
24
+ itsdangerous==2.2.0
25
+ jsonschema==4.25.1
26
+ jsonschema-path==0.3.4
27
+ jsonschema-specifications==2025.9.1
28
+ lazy-object-proxy==1.12.0
29
+ markdown-it-py==4.0.0
30
+ MarkupSafe==3.0.2
31
+ mcp==1.14.0
32
+ mdurl==0.1.2
33
+ more-itertools==10.8.0
34
+ openapi-core==0.19.5
35
+ openapi-pydantic==0.5.1
36
+ openapi-schema-validator==0.6.3
37
+ openapi-spec-validator==0.7.2
38
+ parse==1.20.2
39
+ pathable==0.4.4
40
+ pycparser==2.23
41
+ pydantic==2.11.9
42
+ pydantic-settings==2.10.1
43
+ pydantic_core==2.33.2
44
+ Pygments==2.19.2
45
+ pyperclip==1.9.0
46
+ python-dotenv==1.1.1
47
+ python-multipart==0.0.20
48
+ PyYAML==6.0.2
49
+ referencing==0.36.2
50
+ requests==2.32.5
51
+ rfc3339-validator==0.1.4
52
+ rich==14.1.0
53
+ rich-rst==1.3.1
54
+ rpds-py==0.27.1
55
+ six==1.17.0
56
+ sniffio==1.3.1
57
+ sse-starlette==3.0.2
58
+ starlette==0.47.3
59
+ typing-inspection==0.4.1
60
+ typing_extensions==4.15.0
61
+ urllib3==2.5.0
62
+ uvicorn==0.35.0
63
+ Werkzeug==3.1.1