fastworkflow 2.15.5__py3-none-any.whl → 2.17.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastworkflow/_workflows/command_metadata_extraction/_commands/ErrorCorrection/you_misunderstood.py +1 -1
- fastworkflow/_workflows/command_metadata_extraction/_commands/IntentDetection/what_can_i_do.py +16 -2
- fastworkflow/_workflows/command_metadata_extraction/_commands/wildcard.py +27 -570
- fastworkflow/_workflows/command_metadata_extraction/intent_detection.py +360 -0
- fastworkflow/_workflows/command_metadata_extraction/parameter_extraction.py +411 -0
- fastworkflow/chat_session.py +379 -206
- fastworkflow/cli.py +80 -165
- fastworkflow/command_context_model.py +73 -7
- fastworkflow/command_executor.py +14 -5
- fastworkflow/command_metadata_api.py +106 -6
- fastworkflow/examples/fastworkflow.env +2 -1
- fastworkflow/examples/fastworkflow.passwords.env +2 -1
- fastworkflow/examples/retail_workflow/_commands/exchange_delivered_order_items.py +32 -3
- fastworkflow/examples/retail_workflow/_commands/find_user_id_by_email.py +6 -5
- fastworkflow/examples/retail_workflow/_commands/modify_pending_order_items.py +32 -3
- fastworkflow/examples/retail_workflow/_commands/return_delivered_order_items.py +13 -2
- fastworkflow/examples/retail_workflow/_commands/transfer_to_human_agents.py +1 -1
- fastworkflow/intent_clarification_agent.py +131 -0
- fastworkflow/mcp_server.py +3 -3
- fastworkflow/run/__main__.py +33 -40
- fastworkflow/run_fastapi_mcp/README.md +373 -0
- fastworkflow/run_fastapi_mcp/__main__.py +1300 -0
- fastworkflow/run_fastapi_mcp/conversation_store.py +391 -0
- fastworkflow/run_fastapi_mcp/jwt_manager.py +341 -0
- fastworkflow/run_fastapi_mcp/mcp_specific.py +103 -0
- fastworkflow/run_fastapi_mcp/redoc_2_standalone_html.py +40 -0
- fastworkflow/run_fastapi_mcp/utils.py +517 -0
- fastworkflow/train/__main__.py +1 -1
- fastworkflow/utils/chat_adapter.py +99 -0
- fastworkflow/utils/python_utils.py +4 -4
- fastworkflow/utils/react.py +258 -0
- fastworkflow/utils/signatures.py +338 -139
- fastworkflow/workflow.py +1 -5
- fastworkflow/workflow_agent.py +185 -133
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/METADATA +16 -18
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/RECORD +40 -30
- fastworkflow/run_agent/__main__.py +0 -294
- fastworkflow/run_agent/agent_module.py +0 -194
- /fastworkflow/{run_agent → run_fastapi_mcp}/__init__.py +0 -0
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/LICENSE +0 -0
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/WHEEL +0 -0
- {fastworkflow-2.15.5.dist-info → fastworkflow-2.17.13.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""
|
|
2
|
+
JWT Token Management for FastWorkflow FastAPI Service
|
|
3
|
+
|
|
4
|
+
Handles RSA key pair generation, JWT token creation and verification.
|
|
5
|
+
Keys are stored in ./jwt_keys/ directory.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from datetime import datetime, timedelta, timezone
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from jose import JWTError, jwt
|
|
13
|
+
from jose.constants import ALGORITHMS
|
|
14
|
+
from cryptography.hazmat.primitives import serialization
|
|
15
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
16
|
+
from cryptography.hazmat.backends import default_backend
|
|
17
|
+
|
|
18
|
+
from fastworkflow.utils.logging import logger
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# JWT Configuration (can be made configurable via env vars)
|
|
22
|
+
JWT_ALGORITHM = ALGORITHMS.RS256
|
|
23
|
+
JWT_ACCESS_TOKEN_EXPIRE_MINUTES = 60 # 1 hour
|
|
24
|
+
JWT_REFRESH_TOKEN_EXPIRE_DAYS = 30 # 30 days
|
|
25
|
+
JWT_ISSUER = "fastworkflow-api"
|
|
26
|
+
JWT_AUDIENCE = "fastworkflow-client"
|
|
27
|
+
|
|
28
|
+
# Key storage location (relative to project root)
|
|
29
|
+
KEYS_DIR = "./jwt_keys"
|
|
30
|
+
PRIVATE_KEY_PATH = os.path.join(KEYS_DIR, "private_key.pem")
|
|
31
|
+
PUBLIC_KEY_PATH = os.path.join(KEYS_DIR, "public_key.pem")
|
|
32
|
+
|
|
33
|
+
# In-memory cache for loaded keys
|
|
34
|
+
_private_key: Optional[str] = None
|
|
35
|
+
_public_key: Optional[str] = None
|
|
36
|
+
|
|
37
|
+
# Flag to control JWT verification behavior (set from CLI)
|
|
38
|
+
EXPECT_ENCRYPTED_JWT = True # Default to secure mode
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def ensure_keys_directory() -> None:
|
|
42
|
+
"""Create jwt_keys directory if it doesn't exist."""
|
|
43
|
+
os.makedirs(KEYS_DIR, exist_ok=True)
|
|
44
|
+
logger.info(f"JWT keys directory ensured at: {KEYS_DIR}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def generate_rsa_key_pair() -> tuple[str, str]:
|
|
48
|
+
"""
|
|
49
|
+
Generate a new RSA 2048-bit key pair.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
tuple[str, str]: (private_key_pem, public_key_pem)
|
|
53
|
+
"""
|
|
54
|
+
logger.info("Generating new RSA 2048-bit key pair for JWT...")
|
|
55
|
+
|
|
56
|
+
# Generate private key
|
|
57
|
+
private_key = rsa.generate_private_key(
|
|
58
|
+
public_exponent=65537,
|
|
59
|
+
key_size=2048,
|
|
60
|
+
backend=default_backend()
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Serialize private key to PEM format
|
|
64
|
+
private_pem = private_key.private_bytes(
|
|
65
|
+
encoding=serialization.Encoding.PEM,
|
|
66
|
+
format=serialization.PrivateFormat.PKCS8,
|
|
67
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
68
|
+
).decode('utf-8')
|
|
69
|
+
|
|
70
|
+
# Extract public key and serialize to PEM format
|
|
71
|
+
public_key = private_key.public_key()
|
|
72
|
+
public_pem = public_key.public_bytes(
|
|
73
|
+
encoding=serialization.Encoding.PEM,
|
|
74
|
+
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
|
75
|
+
).decode('utf-8')
|
|
76
|
+
|
|
77
|
+
logger.info("RSA key pair generated successfully")
|
|
78
|
+
return private_pem, public_pem
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def save_keys_to_disk(private_pem: str, public_pem: str) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Save RSA keys to disk with appropriate permissions.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
private_pem: Private key in PEM format
|
|
87
|
+
public_pem: Public key in PEM format
|
|
88
|
+
"""
|
|
89
|
+
ensure_keys_directory()
|
|
90
|
+
|
|
91
|
+
# Save private key (mode 600 for security)
|
|
92
|
+
with open(PRIVATE_KEY_PATH, 'w') as f:
|
|
93
|
+
f.write(private_pem)
|
|
94
|
+
os.chmod(PRIVATE_KEY_PATH, 0o600)
|
|
95
|
+
logger.info(f"Private key saved to: {PRIVATE_KEY_PATH} (mode 600)")
|
|
96
|
+
|
|
97
|
+
# Save public key (mode 644 is fine)
|
|
98
|
+
with open(PUBLIC_KEY_PATH, 'w') as f:
|
|
99
|
+
f.write(public_pem)
|
|
100
|
+
os.chmod(PUBLIC_KEY_PATH, 0o644)
|
|
101
|
+
logger.info(f"Public key saved to: {PUBLIC_KEY_PATH} (mode 644)")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def load_or_generate_keys() -> tuple[str, str]:
|
|
105
|
+
"""
|
|
106
|
+
Load existing RSA keys from disk, or generate new ones if they don't exist.
|
|
107
|
+
Caches keys in memory for performance.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
tuple[str, str]: (private_key_pem, public_key_pem)
|
|
111
|
+
"""
|
|
112
|
+
global _private_key, _public_key
|
|
113
|
+
|
|
114
|
+
# Return cached keys if available
|
|
115
|
+
if _private_key and _public_key:
|
|
116
|
+
return _private_key, _public_key
|
|
117
|
+
|
|
118
|
+
# Try to load existing keys
|
|
119
|
+
if os.path.exists(PRIVATE_KEY_PATH) and os.path.exists(PUBLIC_KEY_PATH):
|
|
120
|
+
logger.info("Loading existing RSA keys from disk...")
|
|
121
|
+
with open(PRIVATE_KEY_PATH, 'r') as f:
|
|
122
|
+
_private_key = f.read()
|
|
123
|
+
with open(PUBLIC_KEY_PATH, 'r') as f:
|
|
124
|
+
_public_key = f.read()
|
|
125
|
+
logger.info("RSA keys loaded successfully")
|
|
126
|
+
else:
|
|
127
|
+
# Generate and save new keys
|
|
128
|
+
logger.info("No existing RSA keys found, generating new ones...")
|
|
129
|
+
_private_key, _public_key = generate_rsa_key_pair()
|
|
130
|
+
save_keys_to_disk(_private_key, _public_key)
|
|
131
|
+
|
|
132
|
+
return _private_key, _public_key
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def set_jwt_verification_mode(expect_encrypted: bool) -> None:
|
|
136
|
+
"""
|
|
137
|
+
Configure JWT verification mode for trusted network scenarios.
|
|
138
|
+
|
|
139
|
+
When expect_encrypted=False, JWT tokens are decoded without signature verification.
|
|
140
|
+
This mode is ONLY suitable for trusted internal networks where JWT is used for
|
|
141
|
+
data transport rather than security.
|
|
142
|
+
|
|
143
|
+
WARNING: Disabling signature verification allows any client to forge tokens.
|
|
144
|
+
Only use in controlled environments.
|
|
145
|
+
"""
|
|
146
|
+
global EXPECT_ENCRYPTED_JWT
|
|
147
|
+
EXPECT_ENCRYPTED_JWT = expect_encrypted
|
|
148
|
+
if not expect_encrypted:
|
|
149
|
+
logger.warning(
|
|
150
|
+
"JWT signature verification DISABLED. "
|
|
151
|
+
"Tokens will be accepted without cryptographic validation. "
|
|
152
|
+
"Only use in trusted internal networks."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def create_access_token(channel_id: str, user_id: Optional[str] = None, expires_days: int | None = None) -> str:
|
|
157
|
+
"""
|
|
158
|
+
Create a JWT access token for a user.
|
|
159
|
+
|
|
160
|
+
Behavior depends on EXPECT_ENCRYPTED_JWT flag:
|
|
161
|
+
- If True: Creates a signed token using RSA algorithm
|
|
162
|
+
- If False: Creates an unsigned token for trusted network use
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
channel_id: Channel identifier (required)
|
|
166
|
+
user_id: User identifier (optional, will be included as uid claim if provided)
|
|
167
|
+
expires_days: Optional custom expiration in days. If None, uses JWT_ACCESS_TOKEN_EXPIRE_MINUTES (default 60 minutes).
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
str: Encoded JWT access token (signed or unsigned based on EXPECT_ENCRYPTED_JWT)
|
|
171
|
+
"""
|
|
172
|
+
now = datetime.now(timezone.utc)
|
|
173
|
+
if expires_days is not None:
|
|
174
|
+
expire = now + timedelta(days=expires_days)
|
|
175
|
+
else:
|
|
176
|
+
expire = now + timedelta(minutes=JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
177
|
+
|
|
178
|
+
# JWT claims
|
|
179
|
+
payload = {
|
|
180
|
+
"sub": channel_id, # Subject: the channel identifier
|
|
181
|
+
"iat": int(now.timestamp()), # Issued at
|
|
182
|
+
"exp": int(expire.timestamp()), # Expiration time
|
|
183
|
+
"jti": f"{channel_id}_{int(now.timestamp())}", # JWT ID (unique identifier)
|
|
184
|
+
"type": "access", # Token type
|
|
185
|
+
"iss": JWT_ISSUER, # Issuer
|
|
186
|
+
"aud": JWT_AUDIENCE # Audience
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Add optional user_id claim
|
|
190
|
+
if user_id is not None:
|
|
191
|
+
payload["uid"] = user_id
|
|
192
|
+
|
|
193
|
+
if EXPECT_ENCRYPTED_JWT:
|
|
194
|
+
# Secure mode: create signed token
|
|
195
|
+
private_key, _ = load_or_generate_keys()
|
|
196
|
+
token = jwt.encode(payload, private_key, algorithm=JWT_ALGORITHM)
|
|
197
|
+
logger.debug(f"Created signed access token for channel_id: {channel_id}, user_id: {user_id}, expires: {expire.isoformat()}")
|
|
198
|
+
else:
|
|
199
|
+
# Trusted network mode: create unsigned token using HS256 with empty key
|
|
200
|
+
# This creates a JWT that can be decoded without verification
|
|
201
|
+
token = jwt.encode(payload, "", algorithm="HS256")
|
|
202
|
+
logger.debug(f"Created unsigned access token for channel_id: {channel_id}, user_id: {user_id}, expires: {expire.isoformat()}")
|
|
203
|
+
|
|
204
|
+
return token
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def create_refresh_token(channel_id: str, user_id: Optional[str] = None) -> str:
|
|
208
|
+
"""
|
|
209
|
+
Create a JWT refresh token for a user.
|
|
210
|
+
|
|
211
|
+
Behavior depends on EXPECT_ENCRYPTED_JWT flag:
|
|
212
|
+
- If True: Creates a signed token using RSA algorithm
|
|
213
|
+
- If False: Creates an unsigned token for trusted network use
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
channel_id: Channel identifier (required)
|
|
217
|
+
user_id: User identifier (optional, will be included as uid claim if provided)
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
str: Encoded JWT refresh token (signed or unsigned based on EXPECT_ENCRYPTED_JWT)
|
|
221
|
+
"""
|
|
222
|
+
now = datetime.now(timezone.utc)
|
|
223
|
+
expire = now + timedelta(days=JWT_REFRESH_TOKEN_EXPIRE_DAYS)
|
|
224
|
+
|
|
225
|
+
# JWT claims
|
|
226
|
+
payload = {
|
|
227
|
+
"sub": channel_id, # Subject: the channel identifier
|
|
228
|
+
"iat": int(now.timestamp()), # Issued at
|
|
229
|
+
"exp": int(expire.timestamp()), # Expiration time
|
|
230
|
+
"jti": f"{channel_id}_{int(now.timestamp())}_refresh", # JWT ID (unique identifier)
|
|
231
|
+
"type": "refresh", # Token type
|
|
232
|
+
"iss": JWT_ISSUER, # Issuer
|
|
233
|
+
"aud": JWT_AUDIENCE # Audience
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Add optional user_id claim
|
|
237
|
+
if user_id is not None:
|
|
238
|
+
payload["uid"] = user_id
|
|
239
|
+
|
|
240
|
+
if EXPECT_ENCRYPTED_JWT:
|
|
241
|
+
# Secure mode: create signed token
|
|
242
|
+
private_key, _ = load_or_generate_keys()
|
|
243
|
+
token = jwt.encode(payload, private_key, algorithm=JWT_ALGORITHM)
|
|
244
|
+
logger.debug(f"Created signed refresh token for channel_id: {channel_id}, user_id: {user_id}, expires: {expire.isoformat()}")
|
|
245
|
+
else:
|
|
246
|
+
# Trusted network mode: create unsigned token using HS256 with empty key
|
|
247
|
+
# This creates a JWT that can be decoded without verification
|
|
248
|
+
token = jwt.encode(payload, "", algorithm="HS256")
|
|
249
|
+
logger.debug(f"Created unsigned refresh token for channel_id: {channel_id}, user_id: {user_id}, expires: {expire.isoformat()}")
|
|
250
|
+
|
|
251
|
+
return token
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def verify_token(token: str, expected_type: str = "access") -> dict:
|
|
255
|
+
# sourcery skip: extract-duplicate-method
|
|
256
|
+
"""
|
|
257
|
+
Verify and decode a JWT token.
|
|
258
|
+
|
|
259
|
+
Behavior depends on EXPECT_ENCRYPTED_JWT flag:
|
|
260
|
+
- If True (default): Full cryptographic verification with signature check
|
|
261
|
+
- If False: Extract payload without verification (trusted network mode)
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
token: JWT token string
|
|
265
|
+
expected_type: Expected token type ("access" or "refresh")
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
dict: Decoded token payload
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
JWTError: If token is invalid, expired, or type mismatch
|
|
272
|
+
"""
|
|
273
|
+
if not EXPECT_ENCRYPTED_JWT:
|
|
274
|
+
# Trusted network mode: decode without verification (accepts both unsigned and signed tokens)
|
|
275
|
+
try:
|
|
276
|
+
# Use unverified decoding - works for any JWT regardless of algorithm or signing
|
|
277
|
+
payload = jwt.get_unverified_claims(token)
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.warning(f"Token decoding failed: {e}")
|
|
280
|
+
raise JWTError(f"Failed to decode token: {e}") from e
|
|
281
|
+
|
|
282
|
+
# Manually check expiration even in unverified mode
|
|
283
|
+
if exp_timestamp := payload.get("exp"):
|
|
284
|
+
import time
|
|
285
|
+
current_time = int(time.time())
|
|
286
|
+
if exp_timestamp < current_time:
|
|
287
|
+
logger.warning(f"Token expired: exp={exp_timestamp}, now={current_time}")
|
|
288
|
+
raise JWTError("Token has expired")
|
|
289
|
+
|
|
290
|
+
# Validate token type for consistency (outside try-except to allow JWTError to propagate)
|
|
291
|
+
if payload.get("type") != expected_type:
|
|
292
|
+
raise JWTError(f"Invalid token type: expected {expected_type}, got {payload.get('type')}")
|
|
293
|
+
|
|
294
|
+
logger.debug(f"Token decoded (unverified mode): channel_id={payload.get('sub')}, type={expected_type}")
|
|
295
|
+
return payload
|
|
296
|
+
|
|
297
|
+
# Standard mode: full verification (existing code)
|
|
298
|
+
_, public_key = load_or_generate_keys()
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
# Decode and verify token
|
|
302
|
+
payload = jwt.decode(
|
|
303
|
+
token,
|
|
304
|
+
public_key,
|
|
305
|
+
algorithms=[JWT_ALGORITHM],
|
|
306
|
+
issuer=JWT_ISSUER,
|
|
307
|
+
audience=JWT_AUDIENCE
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Verify token type
|
|
311
|
+
if payload.get("type") != expected_type:
|
|
312
|
+
raise JWTError(f"Invalid token type: expected {expected_type}, got {payload.get('type')}")
|
|
313
|
+
|
|
314
|
+
logger.debug(f"Token verified successfully: channel_id={payload.get('sub')}, type={expected_type}")
|
|
315
|
+
return payload
|
|
316
|
+
|
|
317
|
+
except JWTError as e:
|
|
318
|
+
logger.warning(f"Token verification failed: {e}")
|
|
319
|
+
raise
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def get_token_expiry(token: str) -> Optional[datetime]:
|
|
323
|
+
"""
|
|
324
|
+
Get the expiration time of a JWT token without full verification.
|
|
325
|
+
Useful for debugging/logging.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
token: JWT token string
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
datetime: Expiration time in UTC, or None if token is invalid
|
|
332
|
+
"""
|
|
333
|
+
try:
|
|
334
|
+
# Decode without verification (just to inspect claims)
|
|
335
|
+
payload = jwt.get_unverified_claims(token)
|
|
336
|
+
if exp_timestamp := payload.get("exp"):
|
|
337
|
+
return datetime.fromtimestamp(exp_timestamp, tz=timezone.utc)
|
|
338
|
+
except Exception as e:
|
|
339
|
+
logger.debug(f"Failed to get token expiry: {e}")
|
|
340
|
+
return None
|
|
341
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# pyright: reportUnusedFunction=false
|
|
2
|
+
|
|
3
|
+
from fastapi_mcp import FastApiMCP
|
|
4
|
+
|
|
5
|
+
def setup_mcp(
|
|
6
|
+
app,
|
|
7
|
+
session_manager,
|
|
8
|
+
):
|
|
9
|
+
"""Mount MCP to automatically convert FastAPI endpoints to MCP tools.
|
|
10
|
+
|
|
11
|
+
FastAPI endpoints are automatically exposed as MCP tools, except those in the exclude list.
|
|
12
|
+
|
|
13
|
+
Key exposed tools:
|
|
14
|
+
- invoke_agent: Streaming agent invocation with NDJSON/SSE support (from /invoke_agent_stream)
|
|
15
|
+
- invoke_assistant: Assistant mode (deterministic execution)
|
|
16
|
+
- new_conversation, get_all_conversations, post_feedback, activate_conversation
|
|
17
|
+
|
|
18
|
+
MCP Client Setup:
|
|
19
|
+
- MCP clients use pre-configured long-lived access tokens (generated via /admin/generate_mcp_token)
|
|
20
|
+
- Tokens are added to the MCP client configuration, not obtained via tool calls
|
|
21
|
+
- No need for initialize or refresh_token tools in MCP context
|
|
22
|
+
|
|
23
|
+
Note: Prompt registration (format-command, clarify-params) is commented out
|
|
24
|
+
as fastapi-mcp 0.4.0 does not support custom prompts.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# =========================================================================
|
|
28
|
+
# Mount MCP (FastApiMCP will scan and find all FastAPI endpoints)
|
|
29
|
+
# =========================================================================
|
|
30
|
+
|
|
31
|
+
# Exclude endpoints that should not be exposed as MCP tools:
|
|
32
|
+
# - root: HTML homepage endpoint
|
|
33
|
+
# - dump_all_conversations: Admin-only endpoint for dumping all user conversations
|
|
34
|
+
# - generate_mcp_token: Admin-only endpoint for generating long-lived MCP tokens
|
|
35
|
+
# - rest_initialize: Regular initialization (MCP clients use pre-configured tokens, don't need to initialize)
|
|
36
|
+
# - perform_action: Low-level action execution (use invoke_agent/invoke_assistant instead)
|
|
37
|
+
# - rest_invoke_agent: Non-streaming version (use "invoke_agent" streaming endpoint instead)
|
|
38
|
+
# - refresh_token: JWT token refresh (not needed for MCP since MCP uses long-lived tokens)
|
|
39
|
+
#
|
|
40
|
+
# Exposed MCP tools:
|
|
41
|
+
# - invoke_agent (operation_id) → /invoke_agent_stream endpoint (streaming with NDJSON/SSE support)
|
|
42
|
+
# - invoke_assistant: Assistant mode (deterministic execution)
|
|
43
|
+
# - new_conversation, get_all_conversations, post_feedback, activate_conversation
|
|
44
|
+
#
|
|
45
|
+
# Note: MCP clients are configured with long-lived access tokens generated via /admin/generate_mcp_token
|
|
46
|
+
mcp = FastApiMCP(
|
|
47
|
+
app,
|
|
48
|
+
exclude_operations=[
|
|
49
|
+
"root",
|
|
50
|
+
"dump_all_conversations",
|
|
51
|
+
"generate_mcp_token",
|
|
52
|
+
"rest_initialize",
|
|
53
|
+
"perform_action",
|
|
54
|
+
"rest_invoke_agent",
|
|
55
|
+
"refresh_token"
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
mcp.mount_http()
|
|
59
|
+
|
|
60
|
+
# Note: Prompt registration is not supported in fastapi-mcp 0.4.0
|
|
61
|
+
# The library automatically converts FastAPI endpoints to MCP tools,
|
|
62
|
+
# but does not provide a way to register custom prompts.
|
|
63
|
+
# Prompts may be added in a future version or via manual MCP server implementation.
|
|
64
|
+
|
|
65
|
+
# TODO: Re-enable when fastapi-mcp supports prompts or implement custom prompt handler
|
|
66
|
+
# # Prompts
|
|
67
|
+
# mcp.add_prompt(
|
|
68
|
+
# name="format-command",
|
|
69
|
+
# description="Given command metadata and a user intent, format a single executable command with XML-tagged parameters.",
|
|
70
|
+
# arguments=[{"name": "intent", "required": True}, {"name": "metadata", "required": True}],
|
|
71
|
+
# handler=lambda intent, metadata: [
|
|
72
|
+
# {
|
|
73
|
+
# "role": "user",
|
|
74
|
+
# "content": {
|
|
75
|
+
# "type": "text",
|
|
76
|
+
# "text": (
|
|
77
|
+
# f"Intent: {intent}\n\nMetadata:\n{metadata}\n\n"
|
|
78
|
+
# "Format a single command: command_name <param>value</param> ..."
|
|
79
|
+
# ),
|
|
80
|
+
# },
|
|
81
|
+
# }
|
|
82
|
+
# ],
|
|
83
|
+
# )
|
|
84
|
+
#
|
|
85
|
+
# mcp.add_prompt(
|
|
86
|
+
# name="clarify-params",
|
|
87
|
+
# description="Compose a concise clarification question for missing parameters using the provided metadata.",
|
|
88
|
+
# arguments=[{"name": "error_message", "required": True}, {"name": "metadata", "required": True}],
|
|
89
|
+
# handler=lambda error_message, metadata: [
|
|
90
|
+
# {
|
|
91
|
+
# "role": "user",
|
|
92
|
+
# "content": {
|
|
93
|
+
# "type": "text",
|
|
94
|
+
# "text": (
|
|
95
|
+
# f"{error_message}\n\nMetadata:\n{metadata}\n\n"
|
|
96
|
+
# "Ask one short question to request the missing parameters."
|
|
97
|
+
# ),
|
|
98
|
+
# },
|
|
99
|
+
# }
|
|
100
|
+
# ],
|
|
101
|
+
# )
|
|
102
|
+
|
|
103
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Script to export the ReDoc documentation page into a standalone HTML file.
|
|
3
|
+
Created by https://github.com/pawamoy on https://github.com/Redocly/redoc/issues/726#issuecomment-645414239
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
from services.run_fastapi.main import app
|
|
10
|
+
|
|
11
|
+
HTML_TEMPLATE = """<!DOCTYPE html>
|
|
12
|
+
<html>
|
|
13
|
+
<head>
|
|
14
|
+
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
|
|
15
|
+
<title>My Project - ReDoc</title>
|
|
16
|
+
<meta charset="utf-8">
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
18
|
+
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
|
|
19
|
+
<style>
|
|
20
|
+
body {
|
|
21
|
+
margin: 0;
|
|
22
|
+
padding: 0;
|
|
23
|
+
}
|
|
24
|
+
</style>
|
|
25
|
+
<style data-styled="" data-styled-version="4.4.1"></style>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<div id="redoc-container"></div>
|
|
29
|
+
<script src="https://cdn.jsdelivr.net/npm/redoc/bundles/redoc.standalone.js"> </script>
|
|
30
|
+
<script>
|
|
31
|
+
var spec = %s;
|
|
32
|
+
Redoc.init(spec, {}, document.getElementById("redoc-container"));
|
|
33
|
+
</script>
|
|
34
|
+
</body>
|
|
35
|
+
</html>
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
with open("redoc.html", "w", encoding="utf-8") as fd:
|
|
40
|
+
print(HTML_TEMPLATE % json.dumps(app.openapi()), file=fd)
|