mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 14:04:18 +02:00
Merge branch 'main' into claude/issue-1647-20250827-0442
This commit is contained in:
commit
d8a921345c
86 changed files with 2349 additions and 1550 deletions
20
.github/workflows/update-config-schema.yml
vendored
20
.github/workflows/update-config-schema.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: Update FastMCPConfig Schema
|
||||
name: Update MCPServerConfig Schema
|
||||
|
||||
# This workflow runs on merges to main to automatically update the config schema
|
||||
# by creating a PR when changes are needed.
|
||||
|
|
@ -7,8 +7,8 @@ on:
|
|||
push:
|
||||
branches: ["main"]
|
||||
paths:
|
||||
- "src/fastmcp/utilities/fastmcp_config/**"
|
||||
- "!src/fastmcp/utilities/fastmcp_config/v1/schema.json" # Exclude the local schema file
|
||||
- "src/fastmcp/utilities/mcp_server_config/**"
|
||||
- "!src/fastmcp/utilities/mcp_server_config/v1/schema.json" # Exclude the local schema file
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
|
@ -45,23 +45,23 @@ jobs:
|
|||
|
||||
# Generate schema in docs/public for web access
|
||||
uv run python -c "
|
||||
from fastmcp.utilities.fastmcp_config import generate_schema
|
||||
from fastmcp.utilities.mcp_server_config import generate_schema
|
||||
generate_schema('docs/public/schemas/fastmcp.json/latest.json')
|
||||
print('✅ Latest schema generated in docs/public')
|
||||
"
|
||||
|
||||
# Also update the v1 schema in docs/public
|
||||
uv run python -c "
|
||||
from fastmcp.utilities.fastmcp_config import generate_schema
|
||||
from fastmcp.utilities.mcp_server_config import generate_schema
|
||||
generate_schema('docs/public/schemas/fastmcp.json/v1.json')
|
||||
print('✅ v1 schema generated in docs/public')
|
||||
"
|
||||
|
||||
# Generate schema in the source directory for local development
|
||||
uv run python -c "
|
||||
from fastmcp.utilities.fastmcp_config import generate_schema
|
||||
generate_schema('src/fastmcp/utilities/fastmcp_config/v1/schema.json')
|
||||
print('✅ Schema generated in utilities/fastmcp_config/v1/')
|
||||
from fastmcp.utilities.mcp_server_config import generate_schema
|
||||
generate_schema('src/fastmcp/utilities/mcp_server_config/v1/schema.json')
|
||||
print('✅ Schema generated in utilities/mcp_server_config/v1/')
|
||||
"
|
||||
|
||||
- name: Create Pull Request
|
||||
|
|
@ -73,12 +73,14 @@ jobs:
|
|||
body: |
|
||||
This PR updates the fastmcp.json schema files to match the current source code.
|
||||
|
||||
The schema is automatically generated from `src/fastmcp/utilities/fastmcp_config/` to ensure consistency.
|
||||
The schema is automatically generated from `src/fastmcp/utilities/mcp_server_config/` to ensure consistency.
|
||||
|
||||
**Note:** This PR is fully automated and will update itself with any subsequent changes to the schema, or close automatically if the schema becomes up-to-date through other means. Feel free to leave it open until you're ready to merge.
|
||||
|
||||
🤖 Generated by Marvin
|
||||
branch: marvin/update-config-schema
|
||||
labels: |
|
||||
ignore in release notes
|
||||
delete-branch: true
|
||||
author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
|
||||
committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
|
||||
|
|
|
|||
2
.github/workflows/update-sdk-docs.yml
vendored
2
.github/workflows/update-sdk-docs.yml
vendored
|
|
@ -62,6 +62,8 @@ jobs:
|
|||
|
||||
🤖 Generated by Marvin
|
||||
branch: marvin/update-sdk-docs
|
||||
labels: |
|
||||
ignore in release notes
|
||||
delete-branch: true
|
||||
author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
|
||||
committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
|
||||
|
|
|
|||
|
|
@ -4,6 +4,60 @@ icon: "list-check"
|
|||
rss: true
|
||||
---
|
||||
|
||||
<Update label="v2.12.0" description="2025-08-31">
|
||||
|
||||
**[v2.12.0: Auth to the Races](https://github.com/jlowin/fastmcp/releases/tag/v2.12.0)**
|
||||
|
||||
This release introduces major authentication and configuration enhancements that make FastMCP more accessible and powerful for developers working with various identity providers and deployment scenarios.
|
||||
|
||||
## OAuth Proxy: Broader Provider Support
|
||||
|
||||
The OAuth Proxy bridges the gap for authentication providers that don't support Dynamic Client Registration (DCR), a requirement for standard MCP OAuth flows. This feature enables seamless integration with major platforms that previously required complex workarounds.
|
||||
|
||||
**Native integrations now available:**
|
||||
- GitHub
|
||||
- Google
|
||||
- WorkOS
|
||||
- Azure
|
||||
|
||||
With the OAuth Proxy, you can authenticate users through these providers with minimal configuration, expanding the ecosystem of supported identity platforms and making FastMCP servers more accessible to enterprise environments.
|
||||
|
||||
## Declarative JSON Configuration
|
||||
|
||||
The new `fastmcp.json` configuration system establishes a single source of truth for server settings, replacing scattered configuration across multiple files and environment variables.
|
||||
|
||||
**Configure everything in one place:**
|
||||
- Dependencies and requirements
|
||||
- Transport settings
|
||||
- Server entrypoints
|
||||
- Metadata and descriptions
|
||||
- Environment variables
|
||||
|
||||
This standardization not only simplifies deployment but also enables portable server descriptions that can be shared and reused across projects. The typed source system provides validation and autocompletion, reducing configuration errors.
|
||||
|
||||
## Sampling API Fallback
|
||||
|
||||
Not all MCP clients support advanced features like the Sampling API for LLM completions. The new fallback mechanism solves this adoption challenge by allowing servers to generate sampling completions server-side when clients lack support.
|
||||
|
||||
This approach:
|
||||
- Maintains compatibility with all clients
|
||||
- Encourages feature adoption without breaking existing integrations
|
||||
- Provides a smooth upgrade path as client capabilities evolve
|
||||
|
||||
## Breaking Changes
|
||||
- The `inspect` command now provides structured output with format options for better integration with tooling
|
||||
|
||||
## Additional Enhancements
|
||||
- Improved CLI configuration parsing with better error messages
|
||||
- Support for multiple `--with-editable` flags for development workflows
|
||||
- Comma-separated OAuth scope support for fine-grained permissions
|
||||
- Configurable logging middleware for better debugging
|
||||
- Support for importing custom route endpoints
|
||||
|
||||
**Full Changelog**: [v2.11.3...v2.12.0](https://github.com/jlowin/fastmcp/compare/v2.11.3...v2.12.0)
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="v2.11.3" description="2025-08-11">
|
||||
|
||||
**[v2.11.3: API-tite for Change](https://github.com/jlowin/fastmcp/releases/tag/v2.11.3)**
|
||||
|
|
|
|||
|
|
@ -44,14 +44,20 @@ This conceptual model helps you understand the purpose of each configuration sec
|
|||
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
|
||||
"source": {
|
||||
// WHERE: Location of your server code
|
||||
"type": "filesystem", // Optional, defaults to "filesystem"
|
||||
"path": "server.py",
|
||||
"entrypoint": "mcp"
|
||||
},
|
||||
"environment": {
|
||||
// WHAT: Python environment and dependencies
|
||||
// WHAT: Environment setup and dependencies
|
||||
"type": "uv", // Optional, defaults to "uv"
|
||||
"python": ">=3.10",
|
||||
"dependencies": ["pandas", "numpy"]
|
||||
},
|
||||
"deployment": {
|
||||
// HOW: Runtime configuration
|
||||
"transport": "stdio",
|
||||
"log_level": "INFO"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -128,15 +134,21 @@ Future releases will support additional source types:
|
|||
|
||||
### Environment Configuration
|
||||
|
||||
The environment configuration determines **WHAT** environment setup your server requires. It controls the build-time setup of your Python environment using `uv`'s powerful dependency management. This section ensures your server runs with the exact Python version and dependencies it requires, creating isolated, reproducible environments across different systems.
|
||||
The environment configuration determines **WHAT** environment setup your server requires. It controls the build-time setup of your Python environment, ensuring your server runs with the exact Python version and dependencies it requires. This section creates isolated, reproducible environments across different systems.
|
||||
|
||||
These settings leverage standard `uv` arguments for environment creation. When any environment field is specified, FastMCP automatically creates an isolated environment before running your server. This build-time configuration happens once when the server starts, not during runtime execution.
|
||||
FastMCP uses an extensible environment system with a base `Environment` class that can be implemented by different environment providers. Currently, FastMCP supports the `UVEnvironment` for Python environment management using `uv`'s powerful dependency resolver.
|
||||
|
||||
<Card icon="code" title="Environment Fields">
|
||||
<Card icon="code" title="Environment">
|
||||
<ParamField body="environment" type="object">
|
||||
Optional Python environment configuration. When any field is specified, FastMCP automatically creates an isolated environment using `uv`.
|
||||
Optional environment configuration. When specified, FastMCP uses the appropriate environment implementation to set up your server's runtime.
|
||||
|
||||
<Expandable title="Environment Fields">
|
||||
<ParamField body="type" type="string" default="uv">
|
||||
The environment type identifier that determines which implementation to use. Currently supports `"uv"` for Python environments managed by uv. If omitted, defaults to `"uv"`.
|
||||
</ParamField>
|
||||
|
||||
<Expandable title="UVEnvironment">
|
||||
When `type` is `"uv"` (or omitted), the environment uses uv to manage Python dependencies:
|
||||
|
||||
<ParamField body="python" type="string">
|
||||
Python version constraint. Examples:
|
||||
- Exact version: `"3.12"`
|
||||
|
|
@ -175,17 +187,36 @@ These settings leverage standard `uv` arguments for environment creation. When a
|
|||
"editable": [".", "../shared-lib", "/path/to/another-package"]
|
||||
```
|
||||
</ParamField>
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
"environment": {
|
||||
"type": "uv",
|
||||
"python": ">=3.10",
|
||||
"dependencies": ["pandas", "numpy"],
|
||||
"editable": ["."]
|
||||
}
|
||||
```
|
||||
|
||||
Note: When any UVEnvironment field is specified, FastMCP automatically creates an isolated environment using `uv` before running your server.
|
||||
</Expandable>
|
||||
</ParamField>
|
||||
</Card>
|
||||
|
||||
When environment configuration is provided, FastMCP:
|
||||
1. Creates an isolated Python environment using `uv`
|
||||
2. Installs the specified dependencies
|
||||
3. Runs your server in this clean environment
|
||||
1. Detects the environment type (defaults to `"uv"` if not specified)
|
||||
2. Creates an isolated environment using the appropriate provider
|
||||
3. Installs the specified dependencies
|
||||
4. Runs your server in this clean environment
|
||||
|
||||
This build-time setup ensures your server always has the dependencies it needs, without polluting your system Python or conflicting with other projects.
|
||||
|
||||
<Note>
|
||||
**Future Environment Types**
|
||||
|
||||
Similar to source types, future releases may support additional environment types for different runtime requirements, such as Docker containers or language-specific environments beyond Python.
|
||||
</Note>
|
||||
|
||||
### Deployment Configuration
|
||||
|
||||
The deployment configuration controls **HOW** your server runs. It defines the runtime behavior including network settings, environment variables, and execution context. These settings determine how your server operates when it executes, from transport protocols to logging levels.
|
||||
|
|
@ -437,6 +468,7 @@ A configuration optimized for local development:
|
|||
},
|
||||
// WHAT dependencies does it need?
|
||||
"environment": {
|
||||
"type": "uv",
|
||||
"python": "3.12",
|
||||
"dependencies": ["fastmcp[dev]"],
|
||||
"editable": "."
|
||||
|
|
|
|||
|
|
@ -133,11 +133,15 @@ async def test_deployed_server():
|
|||
The FastMCP Client handles authentication transparently, making it easy to test secured servers:
|
||||
|
||||
```python
|
||||
from fastmcp.client.transports import StreamableHttpTransport
|
||||
|
||||
async def test_authenticated_server():
|
||||
# Bearer token authentication
|
||||
async with Client(
|
||||
"https://api.example.com/mcp",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
StreamableHttpTransport(
|
||||
"https://api.example.com/mcp",
|
||||
headers={"Authorization": "Bearer test-token"}
|
||||
)
|
||||
) as client:
|
||||
await client.ping()
|
||||
tools = await client.list_tools()
|
||||
|
|
|
|||
|
|
@ -380,33 +380,41 @@
|
|||
"python-sdk/fastmcp-utilities-cli",
|
||||
"python-sdk/fastmcp-utilities-components",
|
||||
"python-sdk/fastmcp-utilities-exceptions",
|
||||
{
|
||||
"group": "fastmcp_config",
|
||||
"pages": [
|
||||
"python-sdk/fastmcp-utilities-fastmcp_config-__init__",
|
||||
{
|
||||
"group": "v1",
|
||||
"pages": [
|
||||
"python-sdk/fastmcp-utilities-fastmcp_config-v1-__init__",
|
||||
"python-sdk/fastmcp-utilities-fastmcp_config-v1-fastmcp_config",
|
||||
{
|
||||
"group": "sources",
|
||||
"pages": [
|
||||
"python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-__init__",
|
||||
"python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-base",
|
||||
"python-sdk/fastmcp-utilities-fastmcp_config-v1-sources-filesystem"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"python-sdk/fastmcp-utilities-http",
|
||||
"python-sdk/fastmcp-utilities-inspect",
|
||||
"python-sdk/fastmcp-utilities-json_schema",
|
||||
"python-sdk/fastmcp-utilities-json_schema_type",
|
||||
"python-sdk/fastmcp-utilities-logging",
|
||||
"python-sdk/fastmcp-utilities-mcp_config",
|
||||
{
|
||||
"group": "mcp_server_config",
|
||||
"pages": [
|
||||
"python-sdk/fastmcp-utilities-mcp_server_config-__init__",
|
||||
{
|
||||
"group": "v1",
|
||||
"pages": [
|
||||
"python-sdk/fastmcp-utilities-mcp_server_config-v1-__init__",
|
||||
{
|
||||
"group": "environments",
|
||||
"pages": [
|
||||
"python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-__init__",
|
||||
"python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base",
|
||||
"python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv"
|
||||
]
|
||||
},
|
||||
"python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config",
|
||||
{
|
||||
"group": "sources",
|
||||
"pages": [
|
||||
"python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-__init__",
|
||||
"python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base",
|
||||
"python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"python-sdk/fastmcp-utilities-openapi",
|
||||
"python-sdk/fastmcp-utilities-tests",
|
||||
"python-sdk/fastmcp-utilities-types"
|
||||
|
|
|
|||
|
|
@ -162,9 +162,49 @@
|
|||
"title": "Deployment",
|
||||
"type": "object"
|
||||
},
|
||||
"Environment": {
|
||||
"FileSystemSource": {
|
||||
"description": "Source for local Python files.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "filesystem",
|
||||
"default": "filesystem",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path to Python file containing the server",
|
||||
"title": "Path",
|
||||
"type": "string"
|
||||
},
|
||||
"entrypoint": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
|
||||
"title": "Entrypoint"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"title": "FileSystemSource",
|
||||
"type": "object"
|
||||
},
|
||||
"UVEnvironment": {
|
||||
"description": "Configuration for Python environment setup.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "uv",
|
||||
"default": "uv",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
},
|
||||
"python": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
@ -266,42 +306,7 @@
|
|||
"title": "Editable"
|
||||
}
|
||||
},
|
||||
"title": "Environment",
|
||||
"type": "object"
|
||||
},
|
||||
"FileSystemSource": {
|
||||
"description": "Source for local Python files.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "filesystem",
|
||||
"default": "filesystem",
|
||||
"description": "Source type",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path to Python file containing the server",
|
||||
"title": "Path",
|
||||
"type": "string"
|
||||
},
|
||||
"entrypoint": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
|
||||
"title": "Entrypoint"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"title": "FileSystemSource",
|
||||
"title": "UVEnvironment",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
|
|
@ -339,7 +344,7 @@
|
|||
]
|
||||
},
|
||||
"environment": {
|
||||
"$ref": "#/$defs/Environment",
|
||||
"$ref": "#/$defs/UVEnvironment",
|
||||
"description": "Python environment setup configuration"
|
||||
},
|
||||
"deployment": {
|
||||
|
|
|
|||
|
|
@ -162,9 +162,49 @@
|
|||
"title": "Deployment",
|
||||
"type": "object"
|
||||
},
|
||||
"Environment": {
|
||||
"FileSystemSource": {
|
||||
"description": "Source for local Python files.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "filesystem",
|
||||
"default": "filesystem",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path to Python file containing the server",
|
||||
"title": "Path",
|
||||
"type": "string"
|
||||
},
|
||||
"entrypoint": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
|
||||
"title": "Entrypoint"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"title": "FileSystemSource",
|
||||
"type": "object"
|
||||
},
|
||||
"UVEnvironment": {
|
||||
"description": "Configuration for Python environment setup.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "uv",
|
||||
"default": "uv",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
},
|
||||
"python": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
@ -266,42 +306,7 @@
|
|||
"title": "Editable"
|
||||
}
|
||||
},
|
||||
"title": "Environment",
|
||||
"type": "object"
|
||||
},
|
||||
"FileSystemSource": {
|
||||
"description": "Source for local Python files.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "filesystem",
|
||||
"default": "filesystem",
|
||||
"description": "Source type",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path to Python file containing the server",
|
||||
"title": "Path",
|
||||
"type": "string"
|
||||
},
|
||||
"entrypoint": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
|
||||
"title": "Entrypoint"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"title": "FileSystemSource",
|
||||
"title": "UVEnvironment",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
|
|
@ -339,7 +344,7 @@
|
|||
]
|
||||
},
|
||||
"environment": {
|
||||
"$ref": "#/$defs/Environment",
|
||||
"$ref": "#/$defs/UVEnvironment",
|
||||
"description": "Python environment setup configuration"
|
||||
},
|
||||
"deployment": {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ FastMCP CLI tools using Cyclopts.
|
|||
|
||||
## Functions
|
||||
|
||||
### `with_argv` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L70" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `with_argv` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
with_argv(args: list[str] | None)
|
||||
|
|
@ -27,7 +27,7 @@ Args are provided without the script name, so we preserve sys.argv[0]
|
|||
and replace the rest.
|
||||
|
||||
|
||||
### `version` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L93" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `version` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L91" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
version()
|
||||
|
|
@ -37,7 +37,7 @@ version()
|
|||
Display version information and platform details.
|
||||
|
||||
|
||||
### `dev` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L131" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `dev` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L129" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
dev(server_spec: str | None = None) -> None
|
||||
|
|
@ -50,7 +50,7 @@ Run an MCP server with the MCP Inspector for development.
|
|||
- `server_spec`: Python file to run, optionally with \:object suffix, or None to auto-detect fastmcp.json
|
||||
|
||||
|
||||
### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L349" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `run` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L310" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
run(server_spec: str | None = None, *server_args: str) -> None
|
||||
|
|
@ -74,7 +74,7 @@ fastmcp run server.py -- --config config.json --debug
|
|||
- `server_spec`: Python file, object specification (file\:obj), config file, URL, or None to auto-detect
|
||||
|
||||
|
||||
### `inspect` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L622" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `inspect` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L524" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
inspect(server_spec: str | None = None) -> None
|
||||
|
|
@ -105,7 +105,7 @@ fastmcp inspect # auto-detect fastmcp.json
|
|||
- `server_spec`: Python file to inspect, optionally with \:object suffix, or fastmcp.json
|
||||
|
||||
|
||||
### `prepare` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L904" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `prepare` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/cli.py#L765" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
prepare(config_path: Annotated[str | None, cyclopts.Parameter(help='Path to fastmcp.json configuration file')] = None, output_dir: Annotated[str | None, cyclopts.Parameter(help='Directory to create the persistent environment in')] = None, skip_source: Annotated[bool, cyclopts.Parameter(help='Skip source preparation (e.g., git clone)')] = False) -> None
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ Install FastMCP server in Claude Code.
|
|||
- True if installation was successful, False otherwise
|
||||
|
||||
|
||||
### `claude_code_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_code.py#L163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `claude_code_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_code.py#L162" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
claude_code_command(server_spec: str) -> None
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ Install FastMCP server in Claude Desktop.
|
|||
- True if installation was successful, False otherwise
|
||||
|
||||
|
||||
### `claude_desktop_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_desktop.py#L135" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `claude_desktop_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/claude_desktop.py#L133" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
claude_desktop_command(server_spec: str) -> None
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ Install FastMCP server to workspace-specific Cursor configuration.
|
|||
- True if installation was successful, False otherwise
|
||||
|
||||
|
||||
### `install_cursor` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L159" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `install_cursor` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L157" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
install_cursor(file: Path, server_object: str | None, name: str) -> bool
|
||||
|
|
@ -93,7 +93,7 @@ Install FastMCP server in Cursor.
|
|||
- True if installation was successful, False otherwise
|
||||
|
||||
|
||||
### `cursor_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L254" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `cursor_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/cursor.py#L250" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
cursor_command(server_spec: str) -> None
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ Generate MCP configuration JSON for manual installation.
|
|||
- True if generation was successful, False otherwise
|
||||
|
||||
|
||||
### `mcp_json_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/mcp_json.py#L108" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `mcp_json_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/install/mcp_json.py#L106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
mcp_json_command(server_spec: str) -> None
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ FastMCP run command implementation with enhanced type hints.
|
|||
|
||||
## Functions
|
||||
|
||||
### `is_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L29" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `is_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L28" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
is_url(path: str) -> bool
|
||||
|
|
@ -20,7 +20,7 @@ is_url(path: str) -> bool
|
|||
Check if a string is a URL.
|
||||
|
||||
|
||||
### `run_with_uv` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L35" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `run_with_uv` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
run_with_uv(server_spec: str, python_version: str | None = None, with_packages: list[str] | None = None, with_requirements: Path | None = None, project: Path | None = None, transport: TransportType | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: LogLevelType | None = None, show_banner: bool = True, editable: str | list[str] | None = None) -> None
|
||||
|
|
@ -29,6 +29,10 @@ run_with_uv(server_spec: str, python_version: str | None = None, with_packages:
|
|||
|
||||
Run a MCP server using uv run subprocess.
|
||||
|
||||
This function is called when we need to set up a Python environment with specific
|
||||
dependencies before running the server. The config parsing and merging should already
|
||||
be done by the caller.
|
||||
|
||||
**Args:**
|
||||
- `server_spec`: Python file, object specification (file\:obj), config file, or URL
|
||||
- `python_version`: Python version to use (e.g. "3.10")
|
||||
|
|
@ -41,9 +45,10 @@ Run a MCP server using uv run subprocess.
|
|||
- `path`: Path to bind to when using http transport
|
||||
- `log_level`: Log level
|
||||
- `show_banner`: Whether to show the server banner
|
||||
- `editable`: Editable package paths
|
||||
|
||||
|
||||
### `create_client_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L170" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `create_client_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L115" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
create_client_server(url: str) -> Any
|
||||
|
|
@ -59,7 +64,7 @@ Create a FastMCP server from a client URL.
|
|||
- A FastMCP server instance
|
||||
|
||||
|
||||
### `create_mcp_config_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L190" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `create_mcp_config_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L135" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]
|
||||
|
|
@ -69,10 +74,10 @@ create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]
|
|||
Create a FastMCP server from a MCPConfig.
|
||||
|
||||
|
||||
### `load_fastmcp_config` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L201" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `load_mcp_server_config` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L146" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
load_fastmcp_config(config_path: Path) -> FastMCPConfig
|
||||
load_mcp_server_config(config_path: Path) -> MCPServerConfig
|
||||
```
|
||||
|
||||
|
||||
|
|
@ -82,10 +87,10 @@ Load a FastMCP configuration from a fastmcp.json file.
|
|||
- `config_path`: Path to fastmcp.json file
|
||||
|
||||
**Returns:**
|
||||
- FastMCPConfig object
|
||||
- MCPServerConfig object
|
||||
|
||||
|
||||
### `run_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L219" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `run_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
run_command(server_spec: str, transport: TransportType | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: LogLevelType | None = None, server_args: list[str] | None = None, show_banner: bool = True, use_direct_import: bool = False, skip_source: bool = False) -> None
|
||||
|
|
@ -107,7 +112,7 @@ Run a MCP server or connect to a remote one.
|
|||
- `skip_source`: Whether to skip source preparation step
|
||||
|
||||
|
||||
### `run_v1_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L341" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `run_v1_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/cli/run.py#L284" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
run_v1_server(server: FastMCP1x, host: str | None = None, port: int | None = None, transport: TransportType | None = None) -> None
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ sidebarTitle: oauth
|
|||
|
||||
## Functions
|
||||
|
||||
### `default_cache_dir` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L36" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `default_cache_dir` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L48" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
default_cache_dir() -> Path
|
||||
```
|
||||
|
||||
### `check_if_auth_required` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L154" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `check_if_auth_required` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L192" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
check_if_auth_required(mcp_url: str, httpx_kwargs: dict[str, Any] | None = None) -> bool
|
||||
|
|
@ -28,7 +28,13 @@ Check if the MCP endpoint requires authentication by making a test request.
|
|||
|
||||
## Classes
|
||||
|
||||
### `FileTokenStorage` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L40" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `StoredToken` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L37" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Token storage format with absolute expiry time.
|
||||
|
||||
|
||||
### `FileTokenStorage` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L52" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
File-based token storage implementation for OAuth credentials and tokens.
|
||||
|
|
@ -39,7 +45,7 @@ Each instance is tied to a specific server URL for proper token isolation.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `get_base_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L55" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_base_url` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_base_url(url: str) -> str
|
||||
|
|
@ -48,7 +54,7 @@ get_base_url(url: str) -> str
|
|||
Extract the base URL (scheme + host) from a URL.
|
||||
|
||||
|
||||
#### `get_cache_key` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L60" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_cache_key` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L72" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_cache_key(self) -> str
|
||||
|
|
@ -57,7 +63,7 @@ get_cache_key(self) -> str
|
|||
Generate a safe filesystem key from the server's base URL.
|
||||
|
||||
|
||||
#### `get_tokens` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L75" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_tokens` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L87" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_tokens(self) -> OAuthToken | None
|
||||
|
|
@ -66,7 +72,7 @@ get_tokens(self) -> OAuthToken | None
|
|||
Load tokens from file storage.
|
||||
|
||||
|
||||
#### `set_tokens` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L92" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `set_tokens` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L119" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_tokens(self, tokens: OAuthToken) -> None
|
||||
|
|
@ -75,7 +81,7 @@ set_tokens(self, tokens: OAuthToken) -> None
|
|||
Save tokens to file storage.
|
||||
|
||||
|
||||
#### `get_client_info` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L98" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_client_info` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L136" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_client_info(self) -> OAuthClientInformationFull | None
|
||||
|
|
@ -84,7 +90,7 @@ get_client_info(self) -> OAuthClientInformationFull | None
|
|||
Load client information from file storage.
|
||||
|
||||
|
||||
#### `set_client_info` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L126" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `set_client_info` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L164" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_client_info(self, client_info: OAuthClientInformationFull) -> None
|
||||
|
|
@ -93,7 +99,7 @@ set_client_info(self, client_info: OAuthClientInformationFull) -> None
|
|||
Save client information to file storage.
|
||||
|
||||
|
||||
#### `clear` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L132" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `clear` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L170" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
clear(self) -> None
|
||||
|
|
@ -102,7 +108,7 @@ clear(self) -> None
|
|||
Clear all cached data for this server.
|
||||
|
||||
|
||||
#### `clear_all` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L141" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `clear_all` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L179" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
clear_all(cls, cache_dir: Path | None = None) -> None
|
||||
|
|
@ -111,7 +117,7 @@ clear_all(cls, cache_dir: Path | None = None) -> None
|
|||
Clear all cached data for all servers.
|
||||
|
||||
|
||||
### `OAuth` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L184" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `OAuth` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L222" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
OAuth client provider for MCP servers with browser-based authentication.
|
||||
|
|
@ -122,7 +128,7 @@ a browser for user authorization and running a local callback server.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `redirect_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L255" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `redirect_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L302" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
redirect_handler(self, authorization_url: str) -> None
|
||||
|
|
@ -131,7 +137,7 @@ redirect_handler(self, authorization_url: str) -> None
|
|||
Open browser for authorization.
|
||||
|
||||
|
||||
#### `callback_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L260" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `callback_handler` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/auth/oauth.py#L307" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
callback_handler(self) -> tuple[str, str | None]
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ sidebarTitle: client
|
|||
|
||||
## Classes
|
||||
|
||||
### `ClientSessionState` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L80" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `ClientSessionState` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L81" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Holds all session-related state for a Client instance.
|
||||
|
|
@ -16,7 +16,7 @@ This allows clean separation of configuration (which is copied) from
|
|||
session state (which should be fresh for each new client instance).
|
||||
|
||||
|
||||
### `Client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L96" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `Client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L97" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
MCP client that delegates connection management to a Transport instance.
|
||||
|
|
@ -79,7 +79,7 @@ async with client:
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L282" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `session` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L283" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
session(self) -> ClientSession
|
||||
|
|
@ -88,7 +88,7 @@ session(self) -> ClientSession
|
|||
Get the current active session. Raises RuntimeError if not connected.
|
||||
|
||||
|
||||
#### `initialize_result` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L292" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `initialize_result` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L293" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
initialize_result(self) -> mcp.types.InitializeResult
|
||||
|
|
@ -97,7 +97,7 @@ initialize_result(self) -> mcp.types.InitializeResult
|
|||
Get the result of the initialization request.
|
||||
|
||||
|
||||
#### `set_roots` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L300" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `set_roots` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L301" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_roots(self, roots: RootsList | RootsHandler) -> None
|
||||
|
|
@ -106,7 +106,7 @@ set_roots(self, roots: RootsList | RootsHandler) -> None
|
|||
Set the roots for the client. This does not automatically call `send_roots_list_changed`.
|
||||
|
||||
|
||||
#### `set_sampling_callback` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L304" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `set_sampling_callback` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L305" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_sampling_callback(self, sampling_callback: ClientSamplingHandler) -> None
|
||||
|
|
@ -115,7 +115,7 @@ set_sampling_callback(self, sampling_callback: ClientSamplingHandler) -> None
|
|||
Set the sampling callback for the client.
|
||||
|
||||
|
||||
#### `set_elicitation_callback` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L310" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `set_elicitation_callback` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L311" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_elicitation_callback(self, elicitation_callback: ElicitationHandler) -> None
|
||||
|
|
@ -124,7 +124,7 @@ set_elicitation_callback(self, elicitation_callback: ElicitationHandler) -> None
|
|||
Set the elicitation callback for the client.
|
||||
|
||||
|
||||
#### `is_connected` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L318" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `is_connected` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L319" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
is_connected(self) -> bool
|
||||
|
|
@ -133,7 +133,7 @@ is_connected(self) -> bool
|
|||
Check if the client is currently connected.
|
||||
|
||||
|
||||
#### `new` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L322" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `new` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L323" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
new(self) -> Client[ClientTransportT]
|
||||
|
|
@ -149,13 +149,13 @@ share state with the original client.
|
|||
- A new Client instance with the same configuration but disconnected state.
|
||||
|
||||
|
||||
#### `close` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L487" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `close` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L490" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
close(self)
|
||||
```
|
||||
|
||||
#### `ping` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L493" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `ping` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L496" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
ping(self) -> bool
|
||||
|
|
@ -164,7 +164,7 @@ ping(self) -> bool
|
|||
Send a ping request.
|
||||
|
||||
|
||||
#### `cancel` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L498" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `cancel` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L501" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
cancel(self, request_id: str | int, reason: str | None = None) -> None
|
||||
|
|
@ -173,7 +173,7 @@ cancel(self, request_id: str | int, reason: str | None = None) -> None
|
|||
Send a cancellation notification for an in-progress request.
|
||||
|
||||
|
||||
#### `progress` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L515" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `progress` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L518" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
progress(self, progress_token: str | int, progress: float, total: float | None = None, message: str | None = None) -> None
|
||||
|
|
@ -182,7 +182,7 @@ progress(self, progress_token: str | int, progress: float, total: float | None =
|
|||
Send a progress notification.
|
||||
|
||||
|
||||
#### `set_logging_level` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L527" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `set_logging_level` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L530" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
set_logging_level(self, level: mcp.types.LoggingLevel) -> None
|
||||
|
|
@ -191,7 +191,7 @@ set_logging_level(self, level: mcp.types.LoggingLevel) -> None
|
|||
Send a logging/setLevel request.
|
||||
|
||||
|
||||
#### `send_roots_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L531" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `send_roots_list_changed` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L534" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
send_roots_list_changed(self) -> None
|
||||
|
|
@ -200,7 +200,7 @@ send_roots_list_changed(self) -> None
|
|||
Send a roots/list_changed notification.
|
||||
|
||||
|
||||
#### `list_resources_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L537" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `list_resources_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L540" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_resources_mcp(self) -> mcp.types.ListResourcesResult
|
||||
|
|
@ -216,7 +216,7 @@ containing the list of resources and any additional metadata.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `list_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L552" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `list_resources` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L555" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_resources(self) -> list[mcp.types.Resource]
|
||||
|
|
@ -231,7 +231,7 @@ Retrieve a list of resources available on the server.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `list_resource_templates_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L564" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `list_resource_templates_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L567" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_resource_templates_mcp(self) -> mcp.types.ListResourceTemplatesResult
|
||||
|
|
@ -247,7 +247,7 @@ containing the list of resource templates and any additional metadata.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `list_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L581" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `list_resource_templates` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L584" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_resource_templates(self) -> list[mcp.types.ResourceTemplate]
|
||||
|
|
@ -262,7 +262,7 @@ Retrieve a list of resource templates available on the server.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `read_resource_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L595" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `read_resource_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L598" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
read_resource_mcp(self, uri: AnyUrl | str) -> mcp.types.ReadResourceResult
|
||||
|
|
@ -281,7 +281,7 @@ containing the resource contents and any additional metadata.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L617" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `read_resource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L620" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
read_resource(self, uri: AnyUrl | str) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]
|
||||
|
|
@ -300,7 +300,7 @@ objects, typically containing either text or binary data.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `list_prompts_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L656" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `list_prompts_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L659" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_prompts_mcp(self) -> mcp.types.ListPromptsResult
|
||||
|
|
@ -316,7 +316,7 @@ containing the list of prompts and any additional metadata.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `list_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L671" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `list_prompts` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L674" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_prompts(self) -> list[mcp.types.Prompt]
|
||||
|
|
@ -331,7 +331,7 @@ Retrieve a list of prompts available on the server.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `get_prompt_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L684" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_prompt_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L687" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_prompt_mcp(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult
|
||||
|
|
@ -351,7 +351,7 @@ containing the prompt messages and any additional metadata.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L720" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_prompt` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L723" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult
|
||||
|
|
@ -371,7 +371,7 @@ containing the prompt messages and any additional metadata.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `complete_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L741" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `complete_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L744" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
complete_mcp(self, ref: mcp.types.ResourceReference | mcp.types.PromptReference, argument: dict[str, str]) -> mcp.types.CompleteResult
|
||||
|
|
@ -391,7 +391,7 @@ containing the completion and any additional metadata.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `complete` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L764" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `complete` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L767" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
complete(self, ref: mcp.types.ResourceReference | mcp.types.PromptReference, argument: dict[str, str]) -> mcp.types.Completion
|
||||
|
|
@ -410,7 +410,7 @@ Send a completion request to the server.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `list_tools_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L786" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `list_tools_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L789" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_tools_mcp(self) -> mcp.types.ListToolsResult
|
||||
|
|
@ -426,7 +426,7 @@ containing the list of tools and any additional metadata.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `list_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L801" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `list_tools` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L804" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
list_tools(self) -> list[mcp.types.Tool]
|
||||
|
|
@ -441,7 +441,7 @@ Retrieve a list of tools available on the server.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `call_tool_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L815" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `call_tool_mcp` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L818" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
call_tool_mcp(self, name: str, arguments: dict[str, Any], progress_handler: ProgressHandler | None = None, timeout: datetime.timedelta | float | int | None = None) -> mcp.types.CallToolResult
|
||||
|
|
@ -466,7 +466,7 @@ containing the tool result and any additional metadata.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L852" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `call_tool` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L855" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
call_tool(self, name: str, arguments: dict[str, Any] | None = None, timeout: datetime.timedelta | float | int | None = None, progress_handler: ProgressHandler | None = None, raise_on_error: bool = True) -> CallToolResult
|
||||
|
|
@ -496,10 +496,10 @@ raw result object.
|
|||
- `RuntimeError`: If called while the client is not connected.
|
||||
|
||||
|
||||
#### `generate_name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L923" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `generate_name` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L926" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
generate_name(cls, name: str | None = None) -> str
|
||||
```
|
||||
|
||||
### `CallToolResult` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L932" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `CallToolResult` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/client/client.py#L935" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ Handles provider-specific requirements:
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `get_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L354" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L330" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_client(self, client_id: str) -> OAuthClientInformationFull | None
|
||||
|
|
@ -199,7 +199,7 @@ handles the case where a client with cached tokens reconnects
|
|||
on a different port.
|
||||
|
||||
|
||||
#### `register_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L386" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `register_client` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L363" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
register_client(self, client_info: OAuthClientInformationFull) -> None
|
||||
|
|
@ -226,7 +226,7 @@ The flow:
|
|||
4. When client reconnects with a different port, ProxyDCRClient accepts it
|
||||
|
||||
|
||||
#### `authorize` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L435" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `authorize` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L422" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
|
||||
|
|
@ -240,7 +240,7 @@ This implements the DCR-compliant proxy pattern:
|
|||
3. Redirect to IdP with our fixed callback URL
|
||||
|
||||
|
||||
#### `load_authorization_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L491" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `load_authorization_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L478" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
|
||||
|
|
@ -252,7 +252,7 @@ Look up our client code and return authorization code object
|
|||
with PKCE challenge for validation.
|
||||
|
||||
|
||||
#### `exchange_authorization_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L533" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `exchange_authorization_code` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L520" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken
|
||||
|
|
@ -264,7 +264,7 @@ For the DCR-compliant proxy flow, we return the IdP tokens that were obtained
|
|||
during the IdP callback exchange. PKCE validation is handled by the MCP framework.
|
||||
|
||||
|
||||
#### `load_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L600" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `load_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L587" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None
|
||||
|
|
@ -273,7 +273,7 @@ load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str)
|
|||
Load refresh token from local storage.
|
||||
|
||||
|
||||
#### `exchange_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L608" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `exchange_refresh_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L595" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken
|
||||
|
|
@ -282,7 +282,7 @@ exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token:
|
|||
Exchange refresh token for new access token using authlib.
|
||||
|
||||
|
||||
#### `load_access_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L683" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `load_access_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L670" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
load_access_token(self, token: str) -> AccessToken | None
|
||||
|
|
@ -294,7 +294,7 @@ Delegates to the JWT verifier which handles signature validation,
|
|||
expiration checking, and claims validation using the upstream JWKS.
|
||||
|
||||
|
||||
#### `revoke_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L700" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `revoke_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L687" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
revoke_token(self, token: AccessToken | RefreshToken) -> None
|
||||
|
|
@ -306,7 +306,7 @@ Removes tokens from local storage and attempts to revoke them with
|
|||
the upstream server if a revocation endpoint is configured.
|
||||
|
||||
|
||||
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L893" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/oauth_proxy.py#L880" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_routes(self) -> list[Route]
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
|
|||
Settings for Azure OAuth provider.
|
||||
|
||||
|
||||
### `AzureTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L46" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `AzureTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L48" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Token verifier for Azure OAuth tokens.
|
||||
|
|
@ -31,7 +31,7 @@ to get user information and validate the token.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L70" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
verify_token(self, token: str) -> AccessToken | None
|
||||
|
|
@ -40,7 +40,7 @@ verify_token(self, token: str) -> AccessToken | None
|
|||
Verify Azure OAuth token by calling Microsoft Graph API.
|
||||
|
||||
|
||||
### `AzureProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L118" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `AzureProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/azure.py#L120" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Azure (Microsoft Entra) OAuth provider for FastMCP.
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ Example:
|
|||
Settings for GitHub OAuth provider.
|
||||
|
||||
|
||||
### `GitHubTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L61" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `GitHubTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Token verifier for GitHub OAuth tokens.
|
||||
|
|
@ -46,7 +46,7 @@ by calling GitHub's API to check if they're valid and get user info.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L83" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L85" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
verify_token(self, token: str) -> AccessToken | None
|
||||
|
|
@ -55,7 +55,7 @@ verify_token(self, token: str) -> AccessToken | None
|
|||
Verify GitHub OAuth token by calling GitHub API.
|
||||
|
||||
|
||||
### `GitHubProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L167" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `GitHubProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/github.py#L169" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Complete GitHub OAuth provider for FastMCP.
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ Example:
|
|||
Settings for Google OAuth provider.
|
||||
|
||||
|
||||
### `GoogleTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `GoogleTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L65" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Token verifier for Google OAuth tokens.
|
||||
|
|
@ -46,7 +46,7 @@ by calling Google's tokeninfo API to check if they're valid and get user info.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L85" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L87" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
verify_token(self, token: str) -> AccessToken | None
|
||||
|
|
@ -55,7 +55,7 @@ verify_token(self, token: str) -> AccessToken | None
|
|||
Verify Google OAuth token by calling Google's tokeninfo API.
|
||||
|
||||
|
||||
### `GoogleProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L183" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `GoogleProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/google.py#L185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Complete Google OAuth provider for FastMCP.
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ Choose based on your WorkOS setup and authentication requirements.
|
|||
Settings for WorkOS OAuth provider.
|
||||
|
||||
|
||||
### `WorkOSTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L53" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `WorkOSTokenVerifier` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L55" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Token verifier for WorkOS OAuth tokens.
|
||||
|
|
@ -35,7 +35,7 @@ the /oauth2/userinfo endpoint to check validity and get user info.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L78" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `verify_token` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L80" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
verify_token(self, token: str) -> AccessToken | None
|
||||
|
|
@ -44,7 +44,7 @@ verify_token(self, token: str) -> AccessToken | None
|
|||
Verify WorkOS OAuth token by calling userinfo endpoint.
|
||||
|
||||
|
||||
### `WorkOSProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L126" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `WorkOSProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L128" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Complete WorkOS OAuth provider for FastMCP.
|
||||
|
|
@ -65,9 +65,9 @@ Setup Requirements:
|
|||
4. Note your Client ID and Client Secret
|
||||
|
||||
|
||||
### `AuthKitProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L253" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `AuthKitProviderSettings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L267" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
### `AuthKitProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L271" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `AuthKitProvider` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L285" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
AuthKit metadata provider for DCR (Dynamic Client Registration).
|
||||
|
|
@ -93,7 +93,7 @@ https://workos.com/docs/authkit/mcp/integrating/token-verification
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L354" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `get_routes` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/auth/providers/workos.py#L368" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
get_routes(self) -> list[Route]
|
||||
|
|
|
|||
|
|
@ -44,8 +44,9 @@ Validate a redirect URI against allowed patterns.
|
|||
|
||||
**Args:**
|
||||
- `redirect_uri`: The redirect URI to validate
|
||||
- `allowed_patterns`: List of allowed patterns. If None, defaults to localhost.
|
||||
If empty list, all URIs are allowed.
|
||||
- `allowed_patterns`: List of allowed patterns. If None, all URIs are allowed (for DCR compatibility).
|
||||
If empty list, no URIs are allowed.
|
||||
To restrict to localhost only, explicitly pass DEFAULT_LOCALHOST_PATTERNS.
|
||||
|
||||
**Returns:**
|
||||
- True if the redirect URI is allowed
|
||||
|
|
|
|||
|
|
@ -8,9 +8,21 @@ sidebarTitle: logging
|
|||
|
||||
Comprehensive logging middleware for FastMCP servers.
|
||||
|
||||
## Functions
|
||||
|
||||
### `default_serializer` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/logging.py#L14" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
default_serializer(data: Any) -> str
|
||||
```
|
||||
|
||||
|
||||
The default serializer for Payloads in the logging middleware.
|
||||
|
||||
|
||||
## Classes
|
||||
|
||||
### `LoggingMiddleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/logging.py#L10" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `LoggingMiddleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/logging.py#L19" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Middleware that provides comprehensive request and response logging.
|
||||
|
|
@ -21,16 +33,16 @@ monitoring, and understanding server usage patterns.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `on_message` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/logging.py#L71" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `on_message` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/logging.py#L91" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any
|
||||
on_message(self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]) -> Any
|
||||
```
|
||||
|
||||
Log all messages.
|
||||
|
||||
|
||||
### `StructuredLoggingMiddleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/logging.py#L92" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `StructuredLoggingMiddleware` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/logging.py#L114" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Middleware that provides structured JSON logging for better log analysis.
|
||||
|
|
@ -41,10 +53,10 @@ aggregation tools like ELK stack, Splunk, or cloud logging services.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `on_message` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/logging.py#L149" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `on_message` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/server/middleware/logging.py#L185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any
|
||||
on_message(self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]) -> Any
|
||||
```
|
||||
|
||||
Log structured message information.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,37 @@ sidebarTitle: cli
|
|||
|
||||
## Functions
|
||||
|
||||
### `log_server_banner` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/cli.py#L27" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `is_already_in_uv_subprocess` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/cli.py#L28" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
is_already_in_uv_subprocess() -> bool
|
||||
```
|
||||
|
||||
|
||||
Check if we're already running in a FastMCP uv subprocess.
|
||||
|
||||
|
||||
### `load_and_merge_config` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/cli.py#L33" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
load_and_merge_config(server_spec: str | None, **cli_overrides) -> tuple[MCPServerConfig, str]
|
||||
```
|
||||
|
||||
|
||||
Load config from server_spec and apply CLI overrides.
|
||||
|
||||
This consolidates the config parsing logic that was duplicated across
|
||||
run, inspect, and dev commands.
|
||||
|
||||
**Args:**
|
||||
- `server_spec`: Python file, config file, URL, or None to auto-detect
|
||||
- `cli_overrides`: CLI arguments that override config values
|
||||
|
||||
**Returns:**
|
||||
- Tuple of (MCPServerConfig, resolved_server_spec)
|
||||
|
||||
|
||||
### `log_server_banner` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/cli.py#L151" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
log_server_banner(server: FastMCP[Any], transport: Literal['stdio', 'http', 'sse', 'streamable-http']) -> None
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ title: __init__
|
|||
sidebarTitle: __init__
|
||||
---
|
||||
|
||||
# `fastmcp.utilities.fastmcp_config`
|
||||
# `fastmcp.utilities.mcp_server_config`
|
||||
|
||||
|
||||
FastMCP Configuration module.
|
||||
|
|
@ -3,6 +3,6 @@ title: __init__
|
|||
sidebarTitle: __init__
|
||||
---
|
||||
|
||||
# `fastmcp.utilities.fastmcp_config.v1`
|
||||
# `fastmcp.utilities.mcp_server_config.v1`
|
||||
|
||||
*This module is empty or contains only private/internal implementations.*
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
title: __init__
|
||||
sidebarTitle: __init__
|
||||
---
|
||||
|
||||
# `fastmcp.utilities.mcp_server_config.v1.environments`
|
||||
|
||||
|
||||
Environment configuration for MCP servers.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
title: base
|
||||
sidebarTitle: base
|
||||
---
|
||||
|
||||
# `fastmcp.utilities.mcp_server_config.v1.environments.base`
|
||||
|
||||
## Classes
|
||||
|
||||
### `Environment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/base.py#L7" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Base class for environment configuration.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `build_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/base.py#L13" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
build_command(self, command: list[str]) -> list[str]
|
||||
```
|
||||
|
||||
Build the full command with environment setup.
|
||||
|
||||
**Args:**
|
||||
- `command`: Base command to wrap with environment setup
|
||||
|
||||
**Returns:**
|
||||
- Full command ready for subprocess execution
|
||||
|
||||
|
||||
#### `prepare` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/base.py#L24" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
prepare(self, output_dir: Path | None = None) -> None
|
||||
```
|
||||
|
||||
Prepare the environment (optional, can be no-op).
|
||||
|
||||
**Args:**
|
||||
- `output_dir`: Directory for persistent environment setup
|
||||
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
title: uv
|
||||
sidebarTitle: uv
|
||||
---
|
||||
|
||||
# `fastmcp.utilities.mcp_server_config.v1.environments.uv`
|
||||
|
||||
## Classes
|
||||
|
||||
### `UVEnvironment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py#L16" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Configuration for Python environment setup.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `build_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py#L51" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
build_command(self, command: list[str]) -> list[str]
|
||||
```
|
||||
|
||||
Build complete uv run command with environment args and command to execute.
|
||||
|
||||
**Args:**
|
||||
- `command`: Command to execute (e.g., ["fastmcp", "run", "server.py"])
|
||||
|
||||
**Returns:**
|
||||
- Complete command ready for subprocess.run, including "uv" prefix if needed.
|
||||
- If no environment configuration is set, returns the command unchanged.
|
||||
|
||||
|
||||
#### `run_with_uv` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py#L95" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
run_with_uv(self, command: list[str]) -> None
|
||||
```
|
||||
|
||||
Execute a command using uv run with this environment configuration.
|
||||
|
||||
**Args:**
|
||||
- `command`: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"])
|
||||
|
||||
|
||||
#### `needs_uv` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py#L136" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
needs_uv(self) -> bool
|
||||
```
|
||||
|
||||
Deprecated: Use _needs_setup() internally or check if build_command modifies the command.
|
||||
|
||||
|
||||
#### `build_uv_run_command` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py#L140" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
build_uv_run_command(self, command: list[str]) -> list[str]
|
||||
```
|
||||
|
||||
Deprecated: Use build_command() instead.
|
||||
|
||||
|
||||
#### `prepare` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py#L144" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
prepare(self, output_dir: Path | None = None) -> None
|
||||
```
|
||||
|
||||
Prepare the Python environment using uv.
|
||||
|
||||
**Args:**
|
||||
- `output_dir`: Directory where the persistent uv project will be created.
|
||||
If None, creates a temporary directory for ephemeral use.
|
||||
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
title: fastmcp_config
|
||||
sidebarTitle: fastmcp_config
|
||||
title: mcp_server_config
|
||||
sidebarTitle: mcp_server_config
|
||||
---
|
||||
|
||||
# `fastmcp.utilities.fastmcp_config.v1.fastmcp_config`
|
||||
# `fastmcp.utilities.mcp_server_config.v1.mcp_server_config`
|
||||
|
||||
|
||||
FastMCP Configuration File Support.
|
||||
|
|
@ -15,7 +15,7 @@ command-line arguments.
|
|||
|
||||
## Functions
|
||||
|
||||
### `generate_schema` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L707" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `generate_schema` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L415" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
generate_schema(output_path: Path | str | None = None) -> dict[str, Any] | None
|
||||
|
|
@ -38,67 +38,7 @@ validation and auto-completion.
|
|||
|
||||
## Classes
|
||||
|
||||
### `Environment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L33" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Configuration for Python environment setup.
|
||||
|
||||
|
||||
**Methods:**
|
||||
|
||||
#### `build_uv_args` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L66" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
build_uv_args(self, command: str | list[str] | None = None) -> list[str]
|
||||
```
|
||||
|
||||
Build uv run arguments from this environment configuration.
|
||||
|
||||
**Args:**
|
||||
- `command`: Optional command to append (string or list of args)
|
||||
|
||||
**Returns:**
|
||||
- List of arguments for uv run command
|
||||
|
||||
|
||||
#### `run_with_uv` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L109" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
run_with_uv(self, command: list[str]) -> None
|
||||
```
|
||||
|
||||
Execute a command using uv run with this environment configuration.
|
||||
|
||||
**Args:**
|
||||
- `command`: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"])
|
||||
|
||||
|
||||
#### `needs_uv` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L132" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
needs_uv(self) -> bool
|
||||
```
|
||||
|
||||
Check if this environment config requires uv to set up.
|
||||
|
||||
**Returns:**
|
||||
- True if any environment settings require uv run
|
||||
|
||||
|
||||
#### `prepare` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L148" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
prepare(self, output_dir: Path | None = None) -> None
|
||||
```
|
||||
|
||||
Prepare the Python environment using uv.
|
||||
|
||||
**Args:**
|
||||
- `output_dir`: Directory where the persistent uv project will be created.
|
||||
If None, creates a temporary directory for ephemeral use.
|
||||
|
||||
|
||||
### `Deployment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L313" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `Deployment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L36" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Configuration for server deployment and runtime settings.
|
||||
|
|
@ -106,7 +46,7 @@ Configuration for server deployment and runtime settings.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `apply_runtime_settings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L362" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `apply_runtime_settings` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L85" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
apply_runtime_settings(self, config_path: Path | None = None) -> None
|
||||
|
|
@ -122,7 +62,7 @@ For example: "API_URL": "https://api.${ENVIRONMENT}.example.com"
|
|||
will substitute the value of the ENVIRONMENT variable at runtime.
|
||||
|
||||
|
||||
### `FastMCPConfig` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L411" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `MCPServerConfig` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L134" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Configuration for a FastMCP server.
|
||||
|
|
@ -133,10 +73,10 @@ a FastMCP server in a declarative format.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `validate_source` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L460" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `validate_source` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L183" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
validate_source(cls, v: dict | FileSystemSource) -> FileSystemSource
|
||||
validate_source(cls, v: dict | Source) -> SourceType
|
||||
```
|
||||
|
||||
Validate and convert source to proper format.
|
||||
|
|
@ -146,23 +86,21 @@ Supports:
|
|||
- FileSystemSource instance (passed through)
|
||||
|
||||
No string parsing happens here - that's only at CLI boundaries.
|
||||
FastMCPConfig works only with properly typed objects.
|
||||
MCPServerConfig works only with properly typed objects.
|
||||
|
||||
|
||||
#### `validate_environment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L483" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `validate_environment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L199" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
validate_environment(cls, v: dict | Environment) -> Environment
|
||||
validate_environment(cls, v: dict | Any) -> EnvironmentType
|
||||
```
|
||||
|
||||
Validate and convert environment to Environment.
|
||||
Ensure environment has a type field for discrimination.
|
||||
|
||||
Accepts:
|
||||
- Environment instance
|
||||
- dict that can be converted to Environment
|
||||
For backward compatibility, if no type is specified, default to "uv".
|
||||
|
||||
|
||||
#### `validate_deployment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L499" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `validate_deployment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L210" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
validate_deployment(cls, v: dict | Deployment) -> Deployment
|
||||
|
|
@ -175,10 +113,10 @@ Accepts:
|
|||
- dict that can be converted to Deployment
|
||||
|
||||
|
||||
#### `from_file` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L515" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `from_file` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L223" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
from_file(cls, file_path: Path) -> FastMCPConfig
|
||||
from_file(cls, file_path: Path) -> MCPServerConfig
|
||||
```
|
||||
|
||||
Load configuration from a JSON file.
|
||||
|
|
@ -187,7 +125,7 @@ Load configuration from a JSON file.
|
|||
- `file_path`: Path to the configuration file
|
||||
|
||||
**Returns:**
|
||||
- FastMCPConfig instance
|
||||
- MCPServerConfig instance
|
||||
|
||||
**Raises:**
|
||||
- `FileNotFoundError`: If the file doesn't exist
|
||||
|
|
@ -195,10 +133,10 @@ Load configuration from a JSON file.
|
|||
- `pydantic.ValidationError`: If the configuration is invalid
|
||||
|
||||
|
||||
#### `from_cli_args` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L538" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `from_cli_args` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L246" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
from_cli_args(cls, source: FileSystemSource, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | None = None, python: str | None = None, dependencies: list[str] | None = None, requirements: str | None = None, project: str | None = None, editable: str | None = None, env: dict[str, str] | None = None, cwd: str | None = None, args: list[str] | None = None) -> FastMCPConfig
|
||||
from_cli_args(cls, source: FileSystemSource, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | None = None, python: str | None = None, dependencies: list[str] | None = None, requirements: str | None = None, project: str | None = None, editable: str | None = None, env: dict[str, str] | None = None, cwd: str | None = None, args: list[str] | None = None) -> MCPServerConfig
|
||||
```
|
||||
|
||||
Create a config from CLI arguments.
|
||||
|
|
@ -223,10 +161,10 @@ goes through a config object.
|
|||
- `args`: Server arguments
|
||||
|
||||
**Returns:**
|
||||
- FastMCPConfig instance
|
||||
- MCPServerConfig instance
|
||||
|
||||
|
||||
#### `find_config` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L615" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `find_config` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L323" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
find_config(cls, start_path: Path | None = None) -> Path | None
|
||||
|
|
@ -241,7 +179,7 @@ Find a fastmcp.json file in the specified directory.
|
|||
- Path to the configuration file, or None if not found
|
||||
|
||||
|
||||
#### `prepare` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L634" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `prepare` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L342" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
prepare(self, skip_source: bool = False, output_dir: Path | None = None) -> None
|
||||
|
|
@ -257,7 +195,7 @@ When output_dir is None, does ephemeral caching (for backwards compatibility).
|
|||
- `output_dir`: Directory to create the persistent uv project in (optional)
|
||||
|
||||
|
||||
#### `prepare_environment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L655" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `prepare_environment` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L363" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
prepare_environment(self, output_dir: Path | None = None) -> None
|
||||
|
|
@ -272,7 +210,7 @@ Prepare the Python environment.
|
|||
Delegates to the environment's prepare() method
|
||||
|
||||
|
||||
#### `prepare_source` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L666" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `prepare_source` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L374" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
prepare_source(self) -> None
|
||||
|
|
@ -283,7 +221,7 @@ Prepare the source for loading.
|
|||
Delegates to the source's prepare() method.
|
||||
|
||||
|
||||
#### `run_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/fastmcp_config.py#L673" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `run_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/mcp_server_config.py#L381" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
run_server(self, **kwargs: Any) -> None
|
||||
|
|
@ -3,6 +3,6 @@ title: __init__
|
|||
sidebarTitle: __init__
|
||||
---
|
||||
|
||||
# `fastmcp.utilities.fastmcp_config.v1.sources`
|
||||
# `fastmcp.utilities.mcp_server_config.v1.sources`
|
||||
|
||||
*This module is empty or contains only private/internal implementations.*
|
||||
|
|
@ -3,11 +3,11 @@ title: base
|
|||
sidebarTitle: base
|
||||
---
|
||||
|
||||
# `fastmcp.utilities.fastmcp_config.v1.sources.base`
|
||||
# `fastmcp.utilities.mcp_server_config.v1.sources.base`
|
||||
|
||||
## Classes
|
||||
|
||||
### `BaseSource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/sources/base.py#L7" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `Source` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/sources/base.py#L7" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Abstract base class for all source types.
|
||||
|
|
@ -15,7 +15,7 @@ Abstract base class for all source types.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `prepare` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/sources/base.py#L12" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `prepare` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/sources/base.py#L12" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
prepare(self) -> None
|
||||
|
|
@ -28,7 +28,7 @@ this method performs that preparation. For sources that don't
|
|||
need preparation (e.g., local files), this is a no-op.
|
||||
|
||||
|
||||
#### `load_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/sources/base.py#L23" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `load_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/sources/base.py#L23" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
load_server(self) -> Any
|
||||
|
|
@ -3,11 +3,11 @@ title: filesystem
|
|||
sidebarTitle: filesystem
|
||||
---
|
||||
|
||||
# `fastmcp.utilities.fastmcp_config.v1.sources.filesystem`
|
||||
# `fastmcp.utilities.mcp_server_config.v1.sources.filesystem`
|
||||
|
||||
## Classes
|
||||
|
||||
### `FileSystemSource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/sources/filesystem.py#L15" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
### `FileSystemSource` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py#L15" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
|
||||
Source for local Python files.
|
||||
|
|
@ -15,7 +15,7 @@ Source for local Python files.
|
|||
|
||||
**Methods:**
|
||||
|
||||
#### `parse_path_with_object` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/sources/filesystem.py#L27" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `parse_path_with_object` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py#L28" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
parse_path_with_object(cls, v: str) -> str
|
||||
|
|
@ -27,7 +27,7 @@ This validator runs before the model is created, allowing us to
|
|||
handle the "file.py:object" syntax at the model boundary.
|
||||
|
||||
|
||||
#### `load_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/fastmcp_config/v1/sources/filesystem.py#L62" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
#### `load_server` <sup><a href="https://github.com/jlowin/fastmcp/blob/main/src/fastmcp/utilities/mcp_server_config/v1/sources/filesystem.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
|
||||
|
||||
```python
|
||||
load_server(self) -> Any
|
||||
|
|
@ -159,19 +159,46 @@ The `OAuthProxy` class provides the complete proxy implementation:
|
|||
</ParamField>
|
||||
|
||||
<ParamField body="resource_server_url" type="AnyHttpUrl | str | None">
|
||||
Resource server URL (defaults to base_url)
|
||||
Path of the FastMCP server (defaults to base_url). **Important**: This should point to your MCP endpoint path. For example, if your MCP server is accessible at `{base_url}/mcp`, specify `https://your-server.com/mcp` here for proper RFC 8707 compliance.
|
||||
</ParamField>
|
||||
|
||||
<ParamField body="allowed_client_redirect_uris" type="list[str] | None">
|
||||
List of allowed redirect URI patterns for MCP clients. Patterns support wildcards (e.g., `"http://localhost:*"`, `"https://*.example.com/*"`).
|
||||
- `None` (default): Only localhost redirect URIs allowed (`http://localhost:*`, `http://127.0.0.1:*`)
|
||||
- Empty list `[]`: All redirect URIs allowed (not recommended for production)
|
||||
- `None` (default): All redirect URIs allowed (for MCP/DCR compatibility)
|
||||
- Empty list `[]`: No redirect URIs allowed
|
||||
- Custom list: Only matching patterns allowed
|
||||
|
||||
These patterns apply to MCP client loopback redirects, NOT the upstream OAuth app redirect URI.
|
||||
</ParamField>
|
||||
</Card>
|
||||
|
||||
### Dynamic client scope
|
||||
|
||||
When `OAuthProxy` creates a dynamic client (`ProxyDCRClient`) during registration or for temporary/unregistered access, it sets the client's `scope` string from your `TokenVerifier.required_scopes` (joined with spaces). If `required_scopes` is empty or `None`, the client's `scope` will be an empty string.
|
||||
|
||||
```python
|
||||
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
||||
from fastmcp.server.auth.oauth_proxy import OAuthProxy
|
||||
|
||||
token_verifier = JWTVerifier(
|
||||
jwks_uri="https://provider/.well-known/jwks.json",
|
||||
issuer="https://provider",
|
||||
audience="my-app",
|
||||
)
|
||||
token_verifier.required_scopes = ["read", "write"]
|
||||
|
||||
auth = OAuthProxy(
|
||||
upstream_authorization_endpoint="https://provider/authorize",
|
||||
upstream_token_endpoint="https://provider/token",
|
||||
upstream_client_id="cid",
|
||||
upstream_client_secret="secret",
|
||||
token_verifier=token_verifier,
|
||||
base_url="https://your-server.com",
|
||||
)
|
||||
|
||||
# Any dynamic client created by the proxy will have scope "read write"
|
||||
```
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.auth import OAuthProxy
|
||||
|
|
@ -201,7 +228,10 @@ auth = OAuthProxy(
|
|||
base_url="https://your-server.com",
|
||||
|
||||
# Optional: customize callback path (defaults to "/auth/callback")
|
||||
redirect_path="/auth/callback"
|
||||
redirect_path="/auth/callback",
|
||||
|
||||
# Optional: specify MCP endpoint path if different from base_url
|
||||
# resource_server_url="https://your-server.com/mcp"
|
||||
)
|
||||
|
||||
mcp = FastMCP(name="My Server", auth=auth)
|
||||
|
|
@ -230,29 +260,45 @@ The proxy automatically:
|
|||
|
||||
## Client Redirect URI Security
|
||||
|
||||
<Warning>
|
||||
By default, OAuth Proxy only accepts localhost redirect URIs from MCP clients for security. You can customize this with the `allowed_client_redirect_uris` parameter:
|
||||
<Note>
|
||||
OAuth Proxy accepts all redirect URIs by default to maintain compatibility with MCP's Dynamic Client Registration (DCR) pattern, where clients register with unpredictable redirect URIs.
|
||||
|
||||
If you know which clients will connect, you can restrict redirect URIs using the `allowed_client_redirect_uris` parameter:
|
||||
|
||||
```python
|
||||
# Default: localhost only (secure)
|
||||
# Default: allow all (for DCR compatibility)
|
||||
auth = OAuthProxy(...)
|
||||
|
||||
# Restrict to localhost only
|
||||
auth = OAuthProxy(
|
||||
...,
|
||||
allowed_client_redirect_uris=[
|
||||
"http://localhost:*",
|
||||
"http://127.0.0.1:*"
|
||||
]
|
||||
)
|
||||
|
||||
# Allow specific known clients (e.g., Claude.ai)
|
||||
auth = OAuthProxy(
|
||||
...,
|
||||
allowed_client_redirect_uris=[
|
||||
"http://localhost:*",
|
||||
"https://claude.ai/api/mcp/auth_callback"
|
||||
]
|
||||
)
|
||||
|
||||
# Custom patterns with wildcards
|
||||
auth = OAuthProxy(
|
||||
...,
|
||||
allowed_client_redirect_uris=[
|
||||
"http://localhost:*",
|
||||
"https://app.example.com/auth/*"
|
||||
"https://*.example.com/auth/*"
|
||||
]
|
||||
)
|
||||
|
||||
# Allow all (NOT recommended for production)
|
||||
auth = OAuthProxy(
|
||||
...,
|
||||
allowed_client_redirect_uris=[]
|
||||
)
|
||||
```
|
||||
</Warning>
|
||||
|
||||
**Tip:** Check your server logs for debug messages that say "Client registered with redirect_uri" messages to see what redirect URIs your clients are using.
|
||||
</Note>
|
||||
|
||||
## Client Compatibility
|
||||
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ token_verifier = JWTVerifier(
|
|||
auth = RemoteAuthProvider(
|
||||
token_verifier=token_verifier,
|
||||
authorization_servers=[AnyHttpUrl("https://auth.yourcompany.com")],
|
||||
resource_server_url="https://api.yourcompany.com",
|
||||
resource_server_url="https://api.yourcompany.com/mcp", # Point to your MCP endpoint
|
||||
# Optional: customize allowed client redirect URIs (defaults to localhost only)
|
||||
allowed_client_redirect_uris=["http://localhost:*", "http://127.0.0.1:*"]
|
||||
)
|
||||
|
|
@ -121,7 +121,7 @@ mcp = FastMCP(name="Company API", auth=auth)
|
|||
|
||||
This configuration creates a server that accepts tokens issued by `auth.yourcompany.com` and provides the OAuth discovery metadata that MCP clients need. The `JWTVerifier` handles token validation using your identity provider's public keys, while the `RemoteAuthProvider` generates the required OAuth endpoints.
|
||||
|
||||
The `authorization_servers` list tells MCP clients which identity providers you trust. The `resource_server_url` identifies your server in OAuth metadata, enabling proper token audience validation.
|
||||
The `authorization_servers` list tells MCP clients which identity providers you trust. The `resource_server_url` identifies your server in OAuth metadata, enabling proper token audience validation. **Important**: The `resource_server_url` should point to your actual MCP endpoint - for example, if your MCP server is accessible at `https://api.yourcompany.com/mcp`, use that full path rather than just the base URL.
|
||||
|
||||
### Custom Endpoints
|
||||
|
||||
|
|
@ -143,7 +143,7 @@ class CompanyAuthProvider(RemoteAuthProvider):
|
|||
super().__init__(
|
||||
token_verifier=token_verifier,
|
||||
authorization_servers=[AnyHttpUrl("https://auth.yourcompany.com")],
|
||||
resource_server_url="https://api.yourcompany.com"
|
||||
resource_server_url="https://api.yourcompany.com/mcp" # Your MCP endpoint path
|
||||
)
|
||||
|
||||
def get_routes(self) -> list[Route]:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ auth = AzureProvider(
|
|||
tenant_id=os.getenv("AZURE_TENANT_ID")
|
||||
or "", # Required for single-tenant apps - get from Azure Portal
|
||||
base_url="http://localhost:8000",
|
||||
resource_server_url="http://localhost:8000/mcp",
|
||||
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ auth = GitHubProvider(
|
|||
client_id=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID") or "",
|
||||
client_secret=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET") or "",
|
||||
base_url="http://localhost:8000",
|
||||
resource_server_url="http://localhost:8000/mcp",
|
||||
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ auth = GoogleProvider(
|
|||
client_id=os.getenv("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID") or "",
|
||||
client_secret=os.getenv("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET") or "",
|
||||
base_url="http://localhost:8000",
|
||||
resource_server_url="http://localhost:8000/mcp",
|
||||
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
|
||||
# Optional: specify required scopes
|
||||
# required_scopes=["openid", "https://www.googleapis.com/auth/userinfo.email"],
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ auth = WorkOSProvider(
|
|||
client_secret=os.getenv("WORKOS_CLIENT_SECRET") or "",
|
||||
authkit_domain=os.getenv("WORKOS_AUTHKIT_DOMAIN") or "https://your-app.authkit.app",
|
||||
base_url="http://localhost:8000",
|
||||
resource_server_url="http://localhost:8000/mcp",
|
||||
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@ With the configuration file in place, you can run the server in several ways:
|
|||
|
||||
```bash
|
||||
# Auto-detect fastmcp.json in current directory
|
||||
cd examples/fastmcp_config_demo
|
||||
cd examples/mcp_server_config_demo
|
||||
fastmcp run
|
||||
|
||||
# Or specify the config file explicitly
|
||||
fastmcp run examples/fastmcp_config_demo/fastmcp.json
|
||||
fastmcp run examples/mcp_server_config_demo/fastmcp.json
|
||||
|
||||
# Or use development mode with the Inspector UI
|
||||
fastmcp dev examples/fastmcp_config_demo/fastmcp.json
|
||||
fastmcp dev examples/mcp_server_config_demo/fastmcp.json
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import sys
|
|||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import Environment
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
|
@ -98,12 +98,11 @@ def update_claude_config(
|
|||
if not deduplicated_packages:
|
||||
deduplicated_packages = None
|
||||
|
||||
# Build uv run command using Environment.build_uv_args()
|
||||
env_config = Environment(
|
||||
# Build uv run command using Environment.build_uv_run_command()
|
||||
env_config = UVEnvironment(
|
||||
dependencies=deduplicated_packages,
|
||||
editable=[str(p) for p in with_editable] if with_editable else None,
|
||||
)
|
||||
args = env_config.build_uv_args()
|
||||
|
||||
# Convert file path to absolute before adding to command
|
||||
# Split off any :object suffix first
|
||||
|
|
@ -113,10 +112,14 @@ def update_claude_config(
|
|||
else:
|
||||
file_spec = str(Path(file_spec).resolve())
|
||||
|
||||
# Add fastmcp run command
|
||||
args.extend(["fastmcp", "run", file_spec])
|
||||
# Build the full command
|
||||
full_command = env_config.build_command(["fastmcp", "run", file_spec])
|
||||
|
||||
server_config: dict[str, Any] = {"command": "uv", "args": args}
|
||||
# Extract command and args for the config
|
||||
server_config: dict[str, Any] = {
|
||||
"command": full_command[0],
|
||||
"args": full_command[1:],
|
||||
}
|
||||
|
||||
# Add environment variables if specified
|
||||
if env_vars:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from typing import Annotated, Literal
|
|||
|
||||
import cyclopts
|
||||
import pyperclip
|
||||
from pydantic import ValidationError
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
|
|
@ -21,15 +20,14 @@ import fastmcp
|
|||
from fastmcp.cli import run as run_module
|
||||
from fastmcp.cli.install import install_app
|
||||
from fastmcp.server.server import FastMCP
|
||||
from fastmcp.utilities.fastmcp_config import Environment, FastMCPConfig
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
from fastmcp.utilities.inspect import (
|
||||
InspectFormat,
|
||||
format_info,
|
||||
inspect_fastmcp,
|
||||
)
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.types import get_cached_typeadapter
|
||||
from fastmcp.utilities.mcp_server_config import MCPServerConfig
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
|
||||
logger = get_logger("cli")
|
||||
console = Console()
|
||||
|
|
@ -195,75 +193,33 @@ async def dev(
|
|||
Args:
|
||||
server_spec: Python file to run, optionally with :object suffix, or None to auto-detect fastmcp.json
|
||||
"""
|
||||
# Convert None to empty lists for list parameters
|
||||
with_editable = with_editable or []
|
||||
with_packages = with_packages or []
|
||||
from pathlib import Path
|
||||
from fastmcp.utilities.cli import load_and_merge_config
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import FastMCPConfig
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
try:
|
||||
# Load config and apply CLI overrides
|
||||
config, server_spec = load_and_merge_config(
|
||||
server_spec,
|
||||
python=python,
|
||||
with_packages=with_packages or [],
|
||||
with_requirements=with_requirements,
|
||||
project=project,
|
||||
editable=[str(p) for p in with_editable] if with_editable else None,
|
||||
port=server_port, # Use deployment config for server port
|
||||
)
|
||||
|
||||
config = None
|
||||
config_path = None
|
||||
# Get server port from config if not specified via CLI
|
||||
if not server_port:
|
||||
server_port = config.deployment.port
|
||||
|
||||
# Auto-detect fastmcp.json if no server_spec provided
|
||||
if server_spec is None:
|
||||
config_path = Path("fastmcp.json")
|
||||
if not config_path.exists():
|
||||
# Check if fastmcp.json exists in current directory
|
||||
found_config = FastMCPConfig.find_config()
|
||||
if found_config:
|
||||
config_path = found_config
|
||||
else:
|
||||
logger.error(
|
||||
"No server specification provided and no fastmcp.json found in current directory.\n"
|
||||
"Please specify a server file or create a fastmcp.json configuration."
|
||||
)
|
||||
sys.exit(1)
|
||||
server_spec = str(config_path)
|
||||
logger.info(f"Using configuration from {config_path}")
|
||||
|
||||
# Create FastMCPConfig from server_spec
|
||||
if server_spec.endswith(".json"):
|
||||
# Load existing config
|
||||
config = FastMCPConfig.from_file(Path(server_spec))
|
||||
|
||||
# Merge environment settings with CLI args (CLI takes precedence)
|
||||
if config.environment:
|
||||
python = python or config.environment.python
|
||||
project = project or (
|
||||
Path(config.environment.project) if config.environment.project else None
|
||||
)
|
||||
with_requirements = with_requirements or (
|
||||
Path(config.environment.requirements)
|
||||
if config.environment.requirements
|
||||
else None
|
||||
)
|
||||
# Merge editable paths from config with CLI args
|
||||
if config.environment.editable and not with_editable:
|
||||
with_editable = [Path(p) for p in config.environment.editable]
|
||||
|
||||
# Merge packages from both sources
|
||||
if config.environment.dependencies:
|
||||
packages = list(config.environment.dependencies)
|
||||
if with_packages:
|
||||
packages.extend(with_packages)
|
||||
with_packages = packages
|
||||
|
||||
# Get server port from deployment config if not specified
|
||||
if config.deployment and config.deployment.port:
|
||||
server_port = server_port or config.deployment.port
|
||||
else:
|
||||
# Create config from file path
|
||||
source = FileSystemSource(path=server_spec)
|
||||
config = FastMCPConfig(source=source)
|
||||
except FileNotFoundError:
|
||||
sys.exit(1)
|
||||
|
||||
logger.debug(
|
||||
"Starting dev server",
|
||||
extra={
|
||||
"server_spec": server_spec,
|
||||
"with_editable": [str(p) for p in with_editable] if with_editable else None,
|
||||
"with_packages": with_packages,
|
||||
"with_editable": config.environment.editable,
|
||||
"with_packages": config.environment.dependencies,
|
||||
"ui_port": ui_port,
|
||||
"server_port": server_port,
|
||||
},
|
||||
|
|
@ -271,6 +227,10 @@ async def dev(
|
|||
|
||||
try:
|
||||
# Load server to check for deprecated dependencies
|
||||
if not config:
|
||||
logger.error("No configuration available")
|
||||
sys.exit(1)
|
||||
assert config is not None # For type checker
|
||||
server: FastMCP = await config.source.load_server()
|
||||
if server.dependencies:
|
||||
import warnings
|
||||
|
|
@ -282,7 +242,13 @@ async def dev(
|
|||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
with_packages = list(set(with_packages + server.dependencies))
|
||||
# Merge server dependencies with environment dependencies
|
||||
env_deps = config.environment.dependencies or []
|
||||
all_deps = list(set(env_deps + server.dependencies))
|
||||
if not config.environment:
|
||||
config.environment = UVEnvironment(dependencies=all_deps)
|
||||
else:
|
||||
config.environment.dependencies = all_deps
|
||||
|
||||
env_vars = {}
|
||||
if ui_port:
|
||||
|
|
@ -303,18 +269,13 @@ async def dev(
|
|||
if inspector_version:
|
||||
inspector_cmd += f"@{inspector_version}"
|
||||
|
||||
# Create Environment object from CLI args
|
||||
env_config = Environment(
|
||||
python=python,
|
||||
dependencies=with_packages if with_packages else None,
|
||||
requirements=str(with_requirements) if with_requirements else None,
|
||||
project=str(project) if project else None,
|
||||
editable=[str(p) for p in with_editable] if with_editable else None,
|
||||
# Use the environment from config (already has CLI overrides applied)
|
||||
uv_cmd = config.environment.build_command(
|
||||
["fastmcp", "run", server_spec, "--no-banner"]
|
||||
)
|
||||
uv_cmd = ["uv"] + env_config.build_uv_args(["fastmcp", "run", server_spec])
|
||||
|
||||
# Add --no-banner flag for dev command
|
||||
uv_cmd.append("--no-banner")
|
||||
# Set marker to prevent infinite loops when subprocess calls FastMCP
|
||||
env = dict(os.environ.items()) | env_vars | {"FASTMCP_UV_SPAWNED": "1"}
|
||||
|
||||
# Run the MCP Inspector command with shell=True on Windows
|
||||
shell = sys.platform == "win32"
|
||||
|
|
@ -322,7 +283,7 @@ async def dev(
|
|||
[npx_cmd, inspector_cmd] + uv_cmd,
|
||||
check=True,
|
||||
shell=shell,
|
||||
env=dict(os.environ.items()) | env_vars,
|
||||
env=env,
|
||||
)
|
||||
sys.exit(process.returncode)
|
||||
except subprocess.CalledProcessError as e:
|
||||
|
|
@ -454,135 +415,76 @@ async def run(
|
|||
Args:
|
||||
server_spec: Python file, object specification (file:obj), config file, URL, or None to auto-detect
|
||||
"""
|
||||
# Convert None to empty lists for list parameters
|
||||
with_packages = with_packages or []
|
||||
# Load configuration if needed
|
||||
from pathlib import Path
|
||||
from fastmcp.utilities.cli import is_already_in_uv_subprocess, load_and_merge_config
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import FastMCPConfig
|
||||
# Check if we were spawned by uv (or user explicitly set --skip-env)
|
||||
if skip_env or is_already_in_uv_subprocess():
|
||||
skip_env = True
|
||||
|
||||
config = None
|
||||
config_path = None
|
||||
editable = None # Initialize editable variable
|
||||
try:
|
||||
# Load config and apply CLI overrides
|
||||
config, server_spec = load_and_merge_config(
|
||||
server_spec,
|
||||
python=python,
|
||||
with_packages=with_packages or [],
|
||||
with_requirements=with_requirements,
|
||||
project=project,
|
||||
transport=transport,
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
log_level=log_level,
|
||||
server_args=list(server_args) if server_args else None,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
sys.exit(1)
|
||||
|
||||
# Auto-detect fastmcp.json if no server_spec provided
|
||||
if server_spec is None:
|
||||
config_path = Path("fastmcp.json")
|
||||
if not config_path.exists():
|
||||
# Check if fastmcp.json exists in current directory
|
||||
found_config = FastMCPConfig.find_config()
|
||||
if found_config:
|
||||
config_path = found_config
|
||||
else:
|
||||
logger.error(
|
||||
"No server specification provided and no fastmcp.json found in current directory.\n"
|
||||
"Please specify a server file or create a fastmcp.json configuration."
|
||||
)
|
||||
sys.exit(1)
|
||||
# Get effective values (CLI overrides take precedence)
|
||||
final_transport = transport or config.deployment.transport
|
||||
final_host = host or config.deployment.host
|
||||
final_port = port or config.deployment.port
|
||||
final_path = path or config.deployment.path
|
||||
final_log_level = log_level or config.deployment.log_level
|
||||
final_server_args = server_args or config.deployment.args
|
||||
|
||||
server_spec = str(config_path)
|
||||
logger.info(f"Using configuration from {config_path}")
|
||||
|
||||
# Load config if server_spec is a .json file
|
||||
if server_spec.endswith(".json"):
|
||||
config_path = Path(server_spec)
|
||||
if config_path.exists():
|
||||
# Try to load as JSON and discriminate between FastMCPConfig and MCPConfig
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Check if it's an MCPConfig first (has canonical mcpServers key)
|
||||
if "mcpServers" in data:
|
||||
# It's an MCPConfig, we don't process these in the run command
|
||||
# They should be handled through different code paths
|
||||
config = None
|
||||
else:
|
||||
# Try to parse as FastMCPConfig
|
||||
try:
|
||||
adapter = get_cached_typeadapter(FastMCPConfig)
|
||||
config = adapter.validate_python(data)
|
||||
|
||||
# Merge deployment config with CLI values (CLI takes precedence)
|
||||
if config.deployment:
|
||||
transport = transport or config.deployment.transport
|
||||
host = host or config.deployment.host
|
||||
port = port or config.deployment.port
|
||||
path = path or config.deployment.path
|
||||
log_level = log_level or config.deployment.log_level
|
||||
server_args = (
|
||||
tuple(server_args)
|
||||
if server_args
|
||||
else tuple(config.deployment.args or ())
|
||||
)
|
||||
|
||||
# Merge environment config with CLI values (CLI takes precedence)
|
||||
# BUT: Skip this if --skip-env is set
|
||||
if config.environment and not skip_env:
|
||||
python = python or config.environment.python
|
||||
project = project or (
|
||||
Path(config.environment.project)
|
||||
if config.environment.project
|
||||
else None
|
||||
)
|
||||
with_requirements = with_requirements or (
|
||||
Path(config.environment.requirements)
|
||||
if config.environment.requirements
|
||||
else None
|
||||
)
|
||||
# Extract editable from config (no CLI override for this)
|
||||
editable = config.environment.editable
|
||||
|
||||
# Merge packages from both sources
|
||||
if config.environment.dependencies:
|
||||
packages = list(config.environment.dependencies)
|
||||
if with_packages:
|
||||
packages.extend(with_packages)
|
||||
with_packages = packages
|
||||
except ValidationError:
|
||||
# Not a valid FastMCPConfig, treat as regular server spec
|
||||
config = None
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
# Not a valid JSON file, treat as regular server spec
|
||||
config = None
|
||||
else:
|
||||
config = None
|
||||
logger.debug(
|
||||
"Running server or client",
|
||||
extra={
|
||||
"server_spec": server_spec,
|
||||
"transport": transport,
|
||||
"host": host,
|
||||
"port": port,
|
||||
"path": path,
|
||||
"log_level": log_level,
|
||||
"server_args": list(server_args),
|
||||
"transport": final_transport,
|
||||
"host": final_host,
|
||||
"port": final_port,
|
||||
"path": final_path,
|
||||
"log_level": final_log_level,
|
||||
"server_args": list(final_server_args) if final_server_args else [],
|
||||
},
|
||||
)
|
||||
|
||||
# Check if we need to use uv run (either from CLI args or config)
|
||||
# When --skip-env is set, we ignore config.environment entirely
|
||||
needs_uv = python or with_packages or with_requirements or project or editable
|
||||
if not needs_uv and config and config.environment and not skip_env:
|
||||
# Check if config's environment needs uv (but only if not skipping env)
|
||||
needs_uv = config.environment.needs_uv()
|
||||
# Check if we need to use uv run (but skip if we're already in uv or user said to skip)
|
||||
# We check if the environment would modify the command
|
||||
test_cmd = ["test"]
|
||||
needs_uv = config.environment.build_command(test_cmd) != test_cmd and not skip_env
|
||||
|
||||
if needs_uv:
|
||||
# Use uv run subprocess - always use run_with_uv which handles output correctly
|
||||
try:
|
||||
run_module.run_with_uv(
|
||||
server_spec=server_spec,
|
||||
python_version=python,
|
||||
with_packages=with_packages,
|
||||
with_requirements=with_requirements,
|
||||
project=project,
|
||||
transport=transport,
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
log_level=log_level,
|
||||
python_version=config.environment.python,
|
||||
with_packages=config.environment.dependencies,
|
||||
with_requirements=Path(config.environment.requirements)
|
||||
if config.environment.requirements
|
||||
else None,
|
||||
project=Path(config.environment.project)
|
||||
if config.environment.project
|
||||
else None,
|
||||
transport=final_transport,
|
||||
host=final_host,
|
||||
port=final_port,
|
||||
path=final_path,
|
||||
log_level=final_log_level,
|
||||
show_banner=not no_banner,
|
||||
editable=editable,
|
||||
editable=config.environment.editable,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
|
|
@ -598,12 +500,12 @@ async def run(
|
|||
try:
|
||||
await run_module.run_command(
|
||||
server_spec=server_spec,
|
||||
transport=transport,
|
||||
host=host,
|
||||
port=port,
|
||||
path=path,
|
||||
log_level=log_level,
|
||||
server_args=list(server_args),
|
||||
transport=final_transport,
|
||||
host=final_host,
|
||||
port=final_port,
|
||||
path=final_path,
|
||||
log_level=final_log_level,
|
||||
server_args=list(final_server_args) if final_server_args else [],
|
||||
show_banner=not no_banner,
|
||||
skip_source=skip_source,
|
||||
)
|
||||
|
|
@ -696,100 +598,49 @@ async def inspect(
|
|||
Args:
|
||||
server_spec: Python file to inspect, optionally with :object suffix, or fastmcp.json
|
||||
"""
|
||||
# Convert None to empty lists for list parameters
|
||||
with_packages = with_packages or []
|
||||
config = None
|
||||
config_path = None
|
||||
from fastmcp.utilities.cli import is_already_in_uv_subprocess, load_and_merge_config
|
||||
|
||||
# Auto-detect fastmcp.json if no server_spec provided
|
||||
if server_spec is None:
|
||||
config_path = Path("fastmcp.json")
|
||||
if not config_path.exists():
|
||||
# Check if fastmcp.json exists in current directory
|
||||
found_config = FastMCPConfig.find_config()
|
||||
if found_config:
|
||||
config_path = found_config
|
||||
else:
|
||||
logger.error(
|
||||
"No server specification provided and no fastmcp.json found in current directory.\n"
|
||||
"Please specify a server file or create a fastmcp.json configuration."
|
||||
)
|
||||
sys.exit(1)
|
||||
# Check if we were spawned by uv (or user explicitly set --skip-env)
|
||||
if skip_env or is_already_in_uv_subprocess():
|
||||
skip_env = True
|
||||
|
||||
server_spec = str(config_path)
|
||||
logger.info(f"Using configuration from {config_path}")
|
||||
try:
|
||||
# Load config and apply CLI overrides
|
||||
config, server_spec = load_and_merge_config(
|
||||
server_spec,
|
||||
python=python,
|
||||
with_packages=with_packages or [],
|
||||
with_requirements=with_requirements,
|
||||
project=project,
|
||||
)
|
||||
|
||||
# Create FastMCPConfig from server_spec
|
||||
if server_spec.endswith(".json"):
|
||||
config_path = Path(server_spec)
|
||||
if config_path.exists():
|
||||
# Check if it's an MCPConfig (which inspect doesn't support)
|
||||
if server_spec.endswith(".json") and config is None:
|
||||
# This might be an MCPConfig, check the file
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
with open(Path(server_spec)) as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Check if it's an MCPConfig (has mcpServers key)
|
||||
if "mcpServers" in data:
|
||||
# MCPConfig - we don't process these in inspect
|
||||
logger.error("MCPConfig files are not supported by inspect command")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# It's a FastMCPConfig
|
||||
config = FastMCPConfig.from_file(config_path)
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# Merge environment settings from config with CLI (CLI takes precedence)
|
||||
if config.environment:
|
||||
python = python or config.environment.python
|
||||
project = project or (
|
||||
Path(config.environment.project)
|
||||
if config.environment.project
|
||||
else None
|
||||
)
|
||||
with_requirements = with_requirements or (
|
||||
Path(config.environment.requirements)
|
||||
if config.environment.requirements
|
||||
else None
|
||||
)
|
||||
except FileNotFoundError:
|
||||
sys.exit(1)
|
||||
|
||||
# Merge packages from both sources
|
||||
if config.environment.dependencies:
|
||||
packages = list(config.environment.dependencies)
|
||||
if with_packages:
|
||||
packages.extend(with_packages)
|
||||
with_packages = packages
|
||||
except (json.JSONDecodeError, ValidationError) as e:
|
||||
logger.error(f"Invalid configuration file: {e}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
logger.error(f"Configuration file not found: {config_path}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# Create config from file path
|
||||
source = FileSystemSource(path=server_spec)
|
||||
config = FastMCPConfig(source=source)
|
||||
|
||||
# Check if we need to use uv run (skip if --skip-env is set)
|
||||
needs_uv = False
|
||||
if not skip_env:
|
||||
needs_uv = python or with_packages or with_requirements or project
|
||||
if not needs_uv and config and config.environment:
|
||||
needs_uv = config.environment.needs_uv()
|
||||
# Check if we need to use uv run (but skip if we're already in uv or user said to skip)
|
||||
# We check if the environment would modify the command
|
||||
test_cmd = ["test"]
|
||||
needs_uv = config.environment.build_command(test_cmd) != test_cmd and not skip_env
|
||||
|
||||
if needs_uv:
|
||||
# Build and run uv command
|
||||
|
||||
# Create or update environment config
|
||||
env_config = Environment(
|
||||
python=python,
|
||||
dependencies=with_packages if with_packages else None,
|
||||
requirements=str(with_requirements) if with_requirements else None,
|
||||
project=str(project) if project else None,
|
||||
)
|
||||
|
||||
# The environment is already configured in the config object
|
||||
inspect_command = [
|
||||
"fastmcp",
|
||||
"inspect",
|
||||
server_spec,
|
||||
"--skip-env", # Prevent infinite loop when calling through uv
|
||||
]
|
||||
|
||||
# Add format and output flags if specified
|
||||
|
|
@ -797,8 +648,14 @@ async def inspect(
|
|||
inspect_command.extend(["--format", format.value])
|
||||
if output:
|
||||
inspect_command.extend(["--output", str(output)])
|
||||
env_config.run_with_uv(inspect_command)
|
||||
return # run_with_uv exits the process
|
||||
|
||||
# Run the command using subprocess
|
||||
import subprocess
|
||||
|
||||
cmd = config.environment.build_command(inspect_command)
|
||||
env = os.environ | {"FASTMCP_UV_SPAWNED": "1"}
|
||||
process = subprocess.run(cmd, check=True, env=env)
|
||||
sys.exit(process.returncode)
|
||||
|
||||
logger.debug(
|
||||
"Inspecting server",
|
||||
|
|
@ -811,6 +668,10 @@ async def inspect(
|
|||
|
||||
try:
|
||||
# Load the server using the config
|
||||
if not config:
|
||||
logger.error("No configuration available")
|
||||
sys.exit(1)
|
||||
assert config is not None # For type checker
|
||||
server = await config.source.load_server()
|
||||
|
||||
# Get basic server information
|
||||
|
|
@ -936,8 +797,6 @@ async def prepare(
|
|||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import FastMCPConfig
|
||||
|
||||
# Require output-dir
|
||||
if output_dir is None:
|
||||
logger.error(
|
||||
|
|
@ -948,7 +807,7 @@ async def prepare(
|
|||
|
||||
# Auto-detect fastmcp.json if not provided
|
||||
if config_path is None:
|
||||
found_config = FastMCPConfig.find_config()
|
||||
found_config = MCPServerConfig.find_config()
|
||||
if found_config:
|
||||
config_path = str(found_config)
|
||||
logger.info(f"Using configuration from {config_path}")
|
||||
|
|
@ -968,7 +827,7 @@ async def prepare(
|
|||
|
||||
try:
|
||||
# Load the configuration
|
||||
config = FastMCPConfig.from_file(config_file)
|
||||
config = MCPServerConfig.from_file(config_file)
|
||||
|
||||
# Prepare environment and source
|
||||
await config.prepare(
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ from typing import Annotated
|
|||
import cyclopts
|
||||
from rich import print
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import Environment
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
|
||||
from .shared import process_common_args
|
||||
|
||||
|
|
@ -115,15 +115,14 @@ def install_claude_code(
|
|||
if not deduplicated_packages:
|
||||
deduplicated_packages = None
|
||||
|
||||
# Build uv run command using Environment.build_uv_args()
|
||||
env_config = Environment(
|
||||
# Build uv run command using Environment.build_uv_run_command()
|
||||
env_config = UVEnvironment(
|
||||
python=python_version,
|
||||
dependencies=deduplicated_packages,
|
||||
requirements=str(with_requirements) if with_requirements else None,
|
||||
project=str(project) if project else None,
|
||||
editable=[str(p) for p in with_editable] if with_editable else None,
|
||||
)
|
||||
args = env_config.build_uv_args()
|
||||
|
||||
# Build server spec from parsed components
|
||||
if server_object:
|
||||
|
|
@ -131,8 +130,8 @@ def install_claude_code(
|
|||
else:
|
||||
server_spec = str(file.resolve())
|
||||
|
||||
# Add fastmcp run command
|
||||
args.extend(["fastmcp", "run", server_spec])
|
||||
# Build the full command
|
||||
full_command = env_config.build_command(["fastmcp", "run", server_spec])
|
||||
|
||||
# Build claude mcp add command
|
||||
cmd_parts = [claude_cmd, "mcp", "add"]
|
||||
|
|
@ -144,7 +143,7 @@ def install_claude_code(
|
|||
|
||||
# Add server name and command
|
||||
cmd_parts.extend([name, "--"])
|
||||
cmd_parts.extend(["uv"] + args)
|
||||
cmd_parts.extend(full_command)
|
||||
|
||||
try:
|
||||
# Run the claude mcp add command
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import cyclopts
|
|||
from rich import print
|
||||
|
||||
from fastmcp.mcp_config import StdioMCPServer, update_config_file
|
||||
from fastmcp.utilities.fastmcp_config import Environment
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
|
||||
from .shared import process_common_args
|
||||
|
||||
|
|
@ -81,28 +81,26 @@ def install_claude_desktop(
|
|||
if not deduplicated_packages:
|
||||
deduplicated_packages = None
|
||||
|
||||
env_config = Environment(
|
||||
env_config = UVEnvironment(
|
||||
python=python_version,
|
||||
dependencies=deduplicated_packages,
|
||||
requirements=str(with_requirements) if with_requirements else None,
|
||||
project=str(project) if project else None,
|
||||
editable=[str(p) for p in with_editable] if with_editable else None,
|
||||
)
|
||||
args = env_config.build_uv_args()
|
||||
|
||||
# Build server spec from parsed components
|
||||
if server_object:
|
||||
server_spec = f"{file.resolve()}:{server_object}"
|
||||
else:
|
||||
server_spec = str(file.resolve())
|
||||
|
||||
# Add fastmcp run command
|
||||
args.extend(["fastmcp", "run", server_spec])
|
||||
# Build the full command
|
||||
full_command = env_config.build_command(["fastmcp", "run", server_spec])
|
||||
|
||||
# Create server configuration
|
||||
server_config = StdioMCPServer(
|
||||
command="uv",
|
||||
args=args,
|
||||
command=full_command[0],
|
||||
args=full_command[1:],
|
||||
env=env_vars or {},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import cyclopts
|
|||
from rich import print
|
||||
|
||||
from fastmcp.mcp_config import StdioMCPServer, update_config_file
|
||||
from fastmcp.utilities.fastmcp_config import Environment
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
|
||||
from .shared import process_common_args
|
||||
|
||||
|
|
@ -115,28 +115,26 @@ def install_cursor_workspace(
|
|||
if not deduplicated_packages:
|
||||
deduplicated_packages = None
|
||||
|
||||
env_config = Environment(
|
||||
env_config = UVEnvironment(
|
||||
python=python_version,
|
||||
dependencies=deduplicated_packages,
|
||||
requirements=str(with_requirements.resolve()) if with_requirements else None,
|
||||
project=str(project.resolve()) if project else None,
|
||||
editable=[str(p.resolve()) for p in with_editable] if with_editable else None,
|
||||
)
|
||||
args = env_config.build_uv_args()
|
||||
|
||||
# Build server spec from parsed components
|
||||
if server_object:
|
||||
server_spec = f"{file.resolve()}:{server_object}"
|
||||
else:
|
||||
server_spec = str(file.resolve())
|
||||
|
||||
# Add fastmcp run command
|
||||
args.extend(["fastmcp", "run", server_spec])
|
||||
# Build the full command
|
||||
full_command = env_config.build_command(["fastmcp", "run", server_spec])
|
||||
|
||||
# Create server configuration
|
||||
server_config = StdioMCPServer(
|
||||
command="uv",
|
||||
args=args,
|
||||
command=full_command[0],
|
||||
args=full_command[1:],
|
||||
env=env_vars or {},
|
||||
)
|
||||
|
||||
|
|
@ -195,23 +193,21 @@ def install_cursor(
|
|||
if not deduplicated_packages:
|
||||
deduplicated_packages = None
|
||||
|
||||
env_config = Environment(
|
||||
env_config = UVEnvironment(
|
||||
python=python_version,
|
||||
dependencies=deduplicated_packages,
|
||||
requirements=str(with_requirements.resolve()) if with_requirements else None,
|
||||
project=str(project.resolve()) if project else None,
|
||||
editable=[str(p.resolve()) for p in with_editable] if with_editable else None,
|
||||
)
|
||||
args = env_config.build_uv_args()
|
||||
|
||||
# Build server spec from parsed components
|
||||
if server_object:
|
||||
server_spec = f"{file.resolve()}:{server_object}"
|
||||
else:
|
||||
server_spec = str(file.resolve())
|
||||
|
||||
# Add fastmcp run command
|
||||
args.extend(["fastmcp", "run", server_spec])
|
||||
# Build the full command
|
||||
full_command = env_config.build_command(["fastmcp", "run", server_spec])
|
||||
|
||||
# If workspace is specified, install to workspace-specific config
|
||||
if workspace:
|
||||
|
|
@ -230,8 +226,8 @@ def install_cursor(
|
|||
|
||||
# Create server configuration
|
||||
server_config = StdioMCPServer(
|
||||
command="uv",
|
||||
args=args,
|
||||
command=full_command[0],
|
||||
args=full_command[1:],
|
||||
env=env_vars or {},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import cyclopts
|
|||
import pyperclip
|
||||
from rich import print
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import Environment
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
|
||||
from .shared import process_common_args
|
||||
|
||||
|
|
@ -56,28 +56,26 @@ def install_mcp_json(
|
|||
if not deduplicated_packages:
|
||||
deduplicated_packages = None
|
||||
|
||||
env_config = Environment(
|
||||
env_config = UVEnvironment(
|
||||
python=python_version,
|
||||
dependencies=deduplicated_packages,
|
||||
requirements=str(with_requirements) if with_requirements else None,
|
||||
project=str(project) if project else None,
|
||||
editable=[str(p) for p in with_editable] if with_editable else None,
|
||||
)
|
||||
args = env_config.build_uv_args()
|
||||
|
||||
# Build server spec from parsed components
|
||||
if server_object:
|
||||
server_spec = f"{file.resolve()}:{server_object}"
|
||||
else:
|
||||
server_spec = str(file.resolve())
|
||||
|
||||
# Add fastmcp run command
|
||||
args.extend(["fastmcp", "run", server_spec])
|
||||
# Build the full command
|
||||
full_command = env_config.build_command(["fastmcp", "run", server_spec])
|
||||
|
||||
# Build MCP server configuration
|
||||
server_config = {
|
||||
"command": "uv",
|
||||
"args": args,
|
||||
"command": full_command[0],
|
||||
"args": full_command[1:],
|
||||
}
|
||||
|
||||
# Add environment variables if provided
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ from dotenv import dotenv_values
|
|||
from pydantic import ValidationError
|
||||
from rich import print
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import FastMCPConfig
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.mcp_server_config import MCPServerConfig
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
|
@ -40,7 +40,7 @@ async def process_common_args(
|
|||
# Convert None to empty lists for list parameters
|
||||
with_packages = with_packages or []
|
||||
env_vars = env_vars or []
|
||||
# Create FastMCPConfig from server_spec
|
||||
# Create MCPServerConfig from server_spec
|
||||
config = None
|
||||
if server_spec.endswith(".json"):
|
||||
config_path = Path(server_spec).resolve()
|
||||
|
|
@ -58,13 +58,13 @@ async def process_common_args(
|
|||
print("[red]MCPConfig files are not supported for installation[/red]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
# It's a FastMCPConfig
|
||||
config = FastMCPConfig.from_file(config_path)
|
||||
# It's a MCPServerConfig
|
||||
config = MCPServerConfig.from_file(config_path)
|
||||
|
||||
# Merge packages from config if not overridden
|
||||
if config.environment and config.environment.dependencies:
|
||||
if config.environment.dependencies:
|
||||
# Merge with CLI packages (CLI takes precedence)
|
||||
config_packages = list(config.environment.dependencies) or []
|
||||
config_packages = list(config.environment.dependencies)
|
||||
with_packages = list(set(with_packages + config_packages))
|
||||
except (json.JSONDecodeError, ValidationError) as e:
|
||||
print(f"[red]Invalid configuration file: {e}[/red]")
|
||||
|
|
@ -72,7 +72,7 @@ async def process_common_args(
|
|||
else:
|
||||
# Create config from file path
|
||||
source = FileSystemSource(path=server_spec)
|
||||
config = FastMCPConfig(source=source)
|
||||
config = MCPServerConfig(source=source)
|
||||
|
||||
# Extract file and server_object from the source
|
||||
# The FileSystemSource handles parsing path:object syntax
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""FastMCP run command implementation with enhanced type hints."""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
|
@ -8,16 +9,14 @@ from pathlib import Path
|
|||
from typing import Any, Literal
|
||||
|
||||
from mcp.server.fastmcp import FastMCP as FastMCP1x
|
||||
from pydantic import ValidationError
|
||||
|
||||
from fastmcp.server.server import FastMCP
|
||||
from fastmcp.utilities.fastmcp_config import (
|
||||
Environment,
|
||||
FastMCPConfig,
|
||||
)
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.types import get_cached_typeadapter
|
||||
from fastmcp.utilities.mcp_server_config import (
|
||||
MCPServerConfig,
|
||||
)
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
|
||||
|
||||
logger = get_logger("cli.run")
|
||||
|
||||
|
|
@ -48,6 +47,10 @@ def run_with_uv(
|
|||
) -> None:
|
||||
"""Run a MCP server using uv run subprocess.
|
||||
|
||||
This function is called when we need to set up a Python environment with specific
|
||||
dependencies before running the server. The config parsing and merging should already
|
||||
be done by the caller.
|
||||
|
||||
Args:
|
||||
server_spec: Python file, object specification (file:obj), config file, or URL
|
||||
python_version: Python version to use (e.g. "3.10")
|
||||
|
|
@ -60,71 +63,11 @@ def run_with_uv(
|
|||
path: Path to bind to when using http transport
|
||||
log_level: Log level
|
||||
show_banner: Whether to show the server banner
|
||||
editable: Editable package paths
|
||||
"""
|
||||
# Check if server_spec is a .json file
|
||||
if server_spec.endswith(".json"):
|
||||
config_path = Path(server_spec).resolve() # Get absolute path
|
||||
if config_path.exists():
|
||||
# Try to load as JSON and discriminate between FastMCPConfig and MCPConfig
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Check if it's an MCPConfig first (has canonical mcpServers key)
|
||||
if "mcpServers" in data:
|
||||
# It's an MCPConfig, we don't process it here - just pass through
|
||||
pass
|
||||
else:
|
||||
# Try to parse as FastMCPConfig
|
||||
try:
|
||||
adapter = get_cached_typeadapter(FastMCPConfig)
|
||||
config: FastMCPConfig = adapter.validate_python(data)
|
||||
|
||||
# Apply deployment settings
|
||||
if config.deployment:
|
||||
config.deployment.apply_runtime_settings(config_path)
|
||||
|
||||
# Merge environment config with CLI args (CLI takes precedence)
|
||||
if config.environment:
|
||||
# Use CLI values if provided, otherwise fall back to config
|
||||
python_version = python_version or config.environment.python
|
||||
project = project or (
|
||||
Path(config.environment.project)
|
||||
if config.environment.project
|
||||
else None
|
||||
)
|
||||
with_requirements = with_requirements or (
|
||||
Path(config.environment.requirements)
|
||||
if config.environment.requirements
|
||||
else None
|
||||
)
|
||||
# Note: config editable is a list but CLI currently only supports single path
|
||||
# Just pass through for now - Environment will handle the list
|
||||
if not editable and config.environment.editable:
|
||||
editable = config.environment.editable
|
||||
|
||||
# Merge packages from both sources
|
||||
# Only merge if with_packages doesn't already contain them
|
||||
# (they may have been merged already in CLI)
|
||||
if config.environment.dependencies and not with_packages:
|
||||
with_packages = list(config.environment.dependencies)
|
||||
|
||||
# Merge deployment config with CLI args (CLI takes precedence)
|
||||
if config.deployment:
|
||||
transport = transport or config.deployment.transport
|
||||
host = host or config.deployment.host
|
||||
port = port or config.deployment.port
|
||||
path = path or config.deployment.path
|
||||
log_level = log_level or config.deployment.log_level
|
||||
except ValidationError:
|
||||
# Not a valid FastMCPConfig, just pass through
|
||||
pass
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
# Not a valid JSON file, just pass through
|
||||
pass
|
||||
|
||||
# Build uv command using Environment.build_uv_args()
|
||||
env_config = Environment(
|
||||
# Build uv command using Environment.build_uv_run_command()
|
||||
env_config = UVEnvironment(
|
||||
python=python_version,
|
||||
dependencies=with_packages if with_packages else None,
|
||||
requirements=str(with_requirements.resolve()) if with_requirements else None,
|
||||
|
|
@ -133,9 +76,9 @@ def run_with_uv(
|
|||
if isinstance(editable, list)
|
||||
else ([editable] if editable else None),
|
||||
)
|
||||
# Build the uv command
|
||||
# Build the inner fastmcp command with --skip-env to prevent infinite recursion
|
||||
inner_cmd = ["fastmcp", "run", "--skip-env", server_spec]
|
||||
|
||||
# Build the inner fastmcp command (environment variable prevents infinite recursion)
|
||||
inner_cmd = ["fastmcp", "run", server_spec]
|
||||
|
||||
# Add transport options to the inner command
|
||||
if transport:
|
||||
|
|
@ -154,13 +97,15 @@ def run_with_uv(
|
|||
inner_cmd.append("--no-banner")
|
||||
|
||||
# Build the full uv command
|
||||
uv_args = env_config.build_uv_args(inner_cmd)
|
||||
cmd = ["uv"] + uv_args
|
||||
cmd = env_config.build_command(inner_cmd)
|
||||
|
||||
# Set marker to prevent infinite loops when subprocess calls FastMCP again
|
||||
env = os.environ | {"FASTMCP_UV_SPAWNED": "1"}
|
||||
|
||||
# Run the command
|
||||
logger.debug(f"Running command: {' '.join(cmd)}")
|
||||
try:
|
||||
process = subprocess.run(cmd, check=True)
|
||||
process = subprocess.run(cmd, check=True, env=env)
|
||||
sys.exit(process.returncode)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to run server: {e}")
|
||||
|
|
@ -198,20 +143,19 @@ def create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]:
|
|||
return server
|
||||
|
||||
|
||||
def load_fastmcp_config(config_path: Path) -> FastMCPConfig:
|
||||
def load_mcp_server_config(config_path: Path) -> MCPServerConfig:
|
||||
"""Load a FastMCP configuration from a fastmcp.json file.
|
||||
|
||||
Args:
|
||||
config_path: Path to fastmcp.json file
|
||||
|
||||
Returns:
|
||||
FastMCPConfig object
|
||||
MCPServerConfig object
|
||||
"""
|
||||
config = FastMCPConfig.from_file(config_path)
|
||||
config = MCPServerConfig.from_file(config_path)
|
||||
|
||||
# Apply runtime settings from deployment config
|
||||
if config.deployment:
|
||||
config.deployment.apply_runtime_settings(config_path)
|
||||
config.deployment.apply_runtime_settings(config_path)
|
||||
|
||||
return config
|
||||
|
||||
|
|
@ -260,18 +204,17 @@ async def run_command(
|
|||
server = create_mcp_config_server(config_path)
|
||||
else:
|
||||
# It's a FastMCP config - load it properly
|
||||
config = load_fastmcp_config(config_path)
|
||||
config = load_mcp_server_config(config_path)
|
||||
|
||||
# Merge deployment config with CLI arguments (CLI takes precedence)
|
||||
if config.deployment:
|
||||
transport = transport or config.deployment.transport
|
||||
host = host or config.deployment.host
|
||||
port = port or config.deployment.port
|
||||
path = path or config.deployment.path
|
||||
log_level = log_level or config.deployment.log_level
|
||||
server_args = (
|
||||
server_args if server_args is not None else config.deployment.args
|
||||
)
|
||||
transport = transport or config.deployment.transport
|
||||
host = host or config.deployment.host
|
||||
port = port or config.deployment.port
|
||||
path = path or config.deployment.path
|
||||
log_level = log_level or config.deployment.log_level
|
||||
server_args = (
|
||||
server_args if server_args is not None else config.deployment.args
|
||||
)
|
||||
|
||||
# Prepare source only (environment is handled by uv run)
|
||||
await config.prepare_source() if not skip_source else None
|
||||
|
|
@ -289,9 +232,9 @@ async def run_command(
|
|||
|
||||
logger.debug(f'Found server "{server.name}" from config {config_path}')
|
||||
else:
|
||||
# Regular file case - create a FastMCPConfig with FileSystemSource
|
||||
# Regular file case - create a MCPServerConfig with FileSystemSource
|
||||
source = FileSystemSource(path=server_spec)
|
||||
config = FastMCPConfig(source=source)
|
||||
config = MCPServerConfig(source=source)
|
||||
|
||||
# Prepare source only (environment is handled by uv run)
|
||||
await config.prepare_source() if not skip_source else None
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import asyncio
|
|||
import json
|
||||
import webbrowser
|
||||
from asyncio import Future
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
from urllib.parse import urlparse
|
||||
|
|
@ -18,7 +19,7 @@ from mcp.shared.auth import (
|
|||
from mcp.shared.auth import (
|
||||
OAuthToken as OAuthToken,
|
||||
)
|
||||
from pydantic import AnyHttpUrl, ValidationError
|
||||
from pydantic import AnyHttpUrl, BaseModel, TypeAdapter, ValidationError
|
||||
from uvicorn.server import Server
|
||||
|
||||
from fastmcp import settings as fastmcp_global_settings
|
||||
|
|
@ -33,6 +34,17 @@ __all__ = ["OAuth"]
|
|||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class StoredToken(BaseModel):
|
||||
"""Token storage format with absolute expiry time."""
|
||||
|
||||
token_payload: OAuthToken
|
||||
expires_at: datetime | None
|
||||
|
||||
|
||||
# Create TypeAdapter at module level for efficient parsing
|
||||
stored_token_adapter = TypeAdapter(StoredToken)
|
||||
|
||||
|
||||
def default_cache_dir() -> Path:
|
||||
return fastmcp_global_settings.home / "oauth-mcp-client-cache"
|
||||
|
||||
|
|
@ -77,13 +89,28 @@ class FileTokenStorage(TokenStorage):
|
|||
path = self._get_file_path("tokens")
|
||||
|
||||
try:
|
||||
tokens = OAuthToken.model_validate_json(path.read_text())
|
||||
# now = datetime.datetime.now(datetime.timezone.utc)
|
||||
# if tokens.expires_at is not None and tokens.expires_at <= now:
|
||||
# logger.debug(f"Token expired for {self.get_base_url(self.server_url)}")
|
||||
# return None
|
||||
return tokens
|
||||
except (FileNotFoundError, json.JSONDecodeError, ValidationError) as e:
|
||||
# Parse JSON and validate as StoredToken
|
||||
stored = stored_token_adapter.validate_json(path.read_text())
|
||||
|
||||
# Check if token is expired
|
||||
if stored.expires_at is not None:
|
||||
now = datetime.now(timezone.utc)
|
||||
if now >= stored.expires_at:
|
||||
logger.debug(
|
||||
f"Token expired for {self.get_base_url(self.server_url)}"
|
||||
)
|
||||
return None
|
||||
|
||||
# Recalculate expires_in to be correct relative to now
|
||||
if stored.token_payload.expires_in is not None:
|
||||
remaining = stored.expires_at - now
|
||||
stored.token_payload.expires_in = max(
|
||||
0, int(remaining.total_seconds())
|
||||
)
|
||||
|
||||
return stored.token_payload
|
||||
|
||||
except (FileNotFoundError, ValidationError) as e:
|
||||
logger.debug(
|
||||
f"Could not load tokens for {self.get_base_url(self.server_url)}: {e}"
|
||||
)
|
||||
|
|
@ -92,7 +119,18 @@ class FileTokenStorage(TokenStorage):
|
|||
async def set_tokens(self, tokens: OAuthToken) -> None:
|
||||
"""Save tokens to file storage."""
|
||||
path = self._get_file_path("tokens")
|
||||
path.write_text(tokens.model_dump_json(indent=2))
|
||||
|
||||
# Calculate absolute expiry time if expires_in is present
|
||||
expires_at = None
|
||||
if tokens.expires_in is not None:
|
||||
expires_at = datetime.now(timezone.utc) + timedelta(
|
||||
seconds=tokens.expires_in
|
||||
)
|
||||
|
||||
# Create StoredToken and save using Pydantic serialization
|
||||
stored = StoredToken(token_payload=tokens, expires_at=expires_at)
|
||||
|
||||
path.write_text(stored.model_dump_json(indent=2))
|
||||
logger.debug(f"Saved tokens for {self.get_base_url(self.server_url)}")
|
||||
|
||||
async def get_client_info(self) -> OAuthClientInformationFull | None:
|
||||
|
|
@ -252,6 +290,15 @@ class OAuth(OAuthClientProvider):
|
|||
callback_handler=self.callback_handler,
|
||||
)
|
||||
|
||||
async def _initialize(self) -> None:
|
||||
"""Load stored tokens and client info, properly setting token expiry."""
|
||||
# Call parent's _initialize to load tokens and client info
|
||||
await super()._initialize()
|
||||
|
||||
# If tokens were loaded and have expires_in, update the context's token_expiry_time
|
||||
if self.context.current_tokens and self.context.current_tokens.expires_in:
|
||||
self.context.update_token_expiry(self.context.current_tokens)
|
||||
|
||||
async def redirect_handler(self, authorization_url: str) -> None:
|
||||
"""Open browser for authorization."""
|
||||
logger.info(f"OAuth authorization URL: {authorization_url}")
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@ from fastmcp.client.auth.oauth import OAuth
|
|||
from fastmcp.mcp_config import MCPConfig, infer_transport_type_from_url
|
||||
from fastmcp.server.dependencies import get_http_headers
|
||||
from fastmcp.server.server import FastMCP
|
||||
from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import Environment
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
|
@ -597,7 +597,7 @@ class UvStdioTransport(StdioTransport):
|
|||
)
|
||||
|
||||
# Create Environment from provided parameters (internal use)
|
||||
env_config = Environment(
|
||||
env_config = UVEnvironment(
|
||||
python=python_version,
|
||||
dependencies=with_packages,
|
||||
requirements=with_requirements,
|
||||
|
|
|
|||
|
|
@ -254,45 +254,20 @@ class OAuthProxy(OAuthProvider):
|
|||
upstream_client_secret: Client secret for upstream server
|
||||
upstream_revocation_endpoint: Optional upstream revocation endpoint
|
||||
token_verifier: Token verifier for validating access tokens
|
||||
base_url: Public URL of this FastMCP server
|
||||
base_url: Public URL of the server that exposes this FastMCP server; redirect path is
|
||||
relative to this URL
|
||||
redirect_path: Redirect path configured in upstream OAuth app (defaults to "/auth/callback")
|
||||
issuer_url: Issuer URL for OAuth metadata (defaults to base_url)
|
||||
service_documentation_url: Optional service documentation URL
|
||||
resource_server_url: Resource server URL (defaults to base_url)
|
||||
resource_server_url: Path of the FastMCP server. If None, FastMCP will
|
||||
attempt to overwrite this with the correct path to the server
|
||||
e.g. {base_url}/mcp
|
||||
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
||||
Patterns support wildcards (e.g., "http://localhost:*", "https://*.example.com/*").
|
||||
If None (default), only localhost redirect URIs are allowed.
|
||||
If empty list, all redirect URIs are allowed (not recommended for production).
|
||||
These are for MCP clients performing loopback redirects, NOT for the upstream OAuth app.
|
||||
"""
|
||||
# Convert string URLs to AnyHttpUrl for parent class
|
||||
base_url_parsed = (
|
||||
AnyHttpUrl(base_url) if isinstance(base_url, str) else base_url
|
||||
)
|
||||
issuer_url_parsed = (
|
||||
(AnyHttpUrl(issuer_url) if isinstance(issuer_url, str) else issuer_url)
|
||||
if issuer_url
|
||||
else None
|
||||
)
|
||||
service_documentation_url_parsed = (
|
||||
(
|
||||
AnyHttpUrl(service_documentation_url)
|
||||
if isinstance(service_documentation_url, str)
|
||||
else service_documentation_url
|
||||
)
|
||||
if service_documentation_url
|
||||
else None
|
||||
)
|
||||
resource_server_url_parsed = (
|
||||
(
|
||||
AnyHttpUrl(resource_server_url)
|
||||
if isinstance(resource_server_url, str)
|
||||
else resource_server_url
|
||||
)
|
||||
if resource_server_url
|
||||
else None
|
||||
)
|
||||
|
||||
# Always enable DCR since we implement it locally for MCP clients
|
||||
client_registration_options = ClientRegistrationOptions(enabled=True)
|
||||
|
||||
|
|
@ -302,13 +277,13 @@ class OAuthProxy(OAuthProvider):
|
|||
)
|
||||
|
||||
super().__init__(
|
||||
base_url=base_url_parsed,
|
||||
issuer_url=issuer_url_parsed,
|
||||
service_documentation_url=service_documentation_url_parsed,
|
||||
base_url=base_url,
|
||||
issuer_url=issuer_url,
|
||||
service_documentation_url=service_documentation_url,
|
||||
client_registration_options=client_registration_options,
|
||||
revocation_options=revocation_options,
|
||||
required_scopes=token_verifier.required_scopes,
|
||||
resource_server_url=resource_server_url_parsed,
|
||||
resource_server_url=resource_server_url,
|
||||
)
|
||||
|
||||
# Store upstream configuration
|
||||
|
|
@ -317,6 +292,7 @@ class OAuthProxy(OAuthProvider):
|
|||
self._upstream_client_id = upstream_client_id
|
||||
self._upstream_client_secret = SecretStr(upstream_client_secret)
|
||||
self._upstream_revocation_endpoint = upstream_revocation_endpoint
|
||||
self._default_scope_str = " ".join(self.required_scopes or [])
|
||||
|
||||
# Store redirect configuration
|
||||
self._redirect_path = (
|
||||
|
|
@ -376,6 +352,7 @@ class OAuthProxy(OAuthProvider):
|
|||
AnyUrl("http://localhost")
|
||||
], # Placeholder, validation uses allowed_patterns
|
||||
grant_types=["authorization_code", "refresh_token"],
|
||||
scope=self._default_scope_str,
|
||||
token_endpoint_auth_method="none",
|
||||
allowed_redirect_uri_patterns=self._allowed_client_redirect_uris,
|
||||
)
|
||||
|
|
@ -415,6 +392,7 @@ class OAuthProxy(OAuthProvider):
|
|||
redirect_uris=client_info.redirect_uris or [AnyUrl("http://localhost")],
|
||||
grant_types=client_info.grant_types
|
||||
or ["authorization_code", "refresh_token"],
|
||||
scope=self._default_scope_str,
|
||||
token_endpoint_auth_method="none",
|
||||
allowed_redirect_uri_patterns=self._allowed_client_redirect_uris,
|
||||
)
|
||||
|
|
@ -422,6 +400,15 @@ class OAuthProxy(OAuthProvider):
|
|||
# Store the ProxyDCRClient using the upstream ID
|
||||
self._clients[upstream_id] = proxy_client
|
||||
|
||||
# Log redirect URIs to help users discover what patterns they might need
|
||||
if client_info.redirect_uris:
|
||||
for uri in client_info.redirect_uris:
|
||||
logger.debug(
|
||||
"Client registered with redirect_uri: %s - if restricting redirect URIs, "
|
||||
"ensure this pattern is allowed in allowed_client_redirect_uris",
|
||||
uri,
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"Registered client %s with %d redirect URIs",
|
||||
upstream_id,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,8 @@ class AzureProviderSettings(BaseSettings):
|
|||
redirect_path: str | None = None
|
||||
required_scopes: list[str] | None = None
|
||||
timeout_seconds: int | None = None
|
||||
resource_server_url: str | None = None
|
||||
allowed_client_redirect_uris: list[str] | None = None
|
||||
|
||||
@field_validator("required_scopes", mode="before")
|
||||
@classmethod
|
||||
|
|
@ -160,6 +162,8 @@ class AzureProvider(OAuthProxy):
|
|||
redirect_path: str | NotSetT = NotSet,
|
||||
required_scopes: list[str] | None | NotSetT = NotSet,
|
||||
timeout_seconds: int | NotSetT = NotSet,
|
||||
resource_server_url: str | NotSetT = NotSet,
|
||||
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
||||
):
|
||||
"""Initialize Azure OAuth provider.
|
||||
|
||||
|
|
@ -171,6 +175,10 @@ class AzureProvider(OAuthProxy):
|
|||
redirect_path: Redirect path configured in Azure (defaults to "/auth/callback")
|
||||
required_scopes: Required scopes (defaults to ["User.Read", "email", "openid", "profile"])
|
||||
timeout_seconds: HTTP request timeout for Azure API calls
|
||||
resource_server_url: Path of the FastMCP server (defaults to base_url). If your MCP endpoint is at
|
||||
a different path like {base_url}/mcp, specify it here for RFC 8707 compliance.
|
||||
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
||||
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
||||
"""
|
||||
settings = AzureProviderSettings.model_validate(
|
||||
{
|
||||
|
|
@ -183,6 +191,8 @@ class AzureProvider(OAuthProxy):
|
|||
"redirect_path": redirect_path,
|
||||
"required_scopes": required_scopes,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
"resource_server_url": resource_server_url,
|
||||
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
||||
}.items()
|
||||
if v is not NotSet
|
||||
}
|
||||
|
|
@ -217,6 +227,8 @@ class AzureProvider(OAuthProxy):
|
|||
"openid",
|
||||
"profile",
|
||||
]
|
||||
resource_server_url_final = settings.resource_server_url or base_url_final
|
||||
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
|
||||
|
||||
# Extract secret string from SecretStr
|
||||
client_secret_str = (
|
||||
|
|
@ -247,6 +259,8 @@ class AzureProvider(OAuthProxy):
|
|||
base_url=base_url_final,
|
||||
redirect_path=redirect_path_final,
|
||||
issuer_url=base_url_final,
|
||||
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
||||
resource_server_url=resource_server_url_final,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ class GitHubProviderSettings(BaseSettings):
|
|||
redirect_path: str | None = None
|
||||
required_scopes: list[str] | None = None
|
||||
timeout_seconds: int | None = None
|
||||
resource_server_url: AnyHttpUrl | str | None = None
|
||||
allowed_client_redirect_uris: list[str] | None = None
|
||||
|
||||
@field_validator("required_scopes", mode="before")
|
||||
@classmethod
|
||||
|
|
@ -199,8 +201,10 @@ class GitHubProvider(OAuthProxy):
|
|||
client_secret: str | NotSetT = NotSet,
|
||||
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
||||
redirect_path: str | NotSetT = NotSet,
|
||||
required_scopes: list[str] | None | NotSetT = NotSet,
|
||||
required_scopes: list[str] | NotSetT = NotSet,
|
||||
timeout_seconds: int | NotSetT = NotSet,
|
||||
resource_server_url: AnyHttpUrl | str | NotSetT = NotSet,
|
||||
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
||||
):
|
||||
"""Initialize GitHub OAuth provider.
|
||||
|
||||
|
|
@ -211,6 +215,10 @@ class GitHubProvider(OAuthProxy):
|
|||
redirect_path: Redirect path configured in GitHub OAuth app (defaults to "/auth/callback")
|
||||
required_scopes: Required GitHub scopes (defaults to ["user"])
|
||||
timeout_seconds: HTTP request timeout for GitHub API calls
|
||||
resource_server_url: Path of the FastMCP server (defaults to base_url). If your MCP endpoint is at
|
||||
a different path like {base_url}/mcp, specify it here for RFC 8707 compliance.
|
||||
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
||||
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
||||
"""
|
||||
settings = GitHubProviderSettings.model_validate(
|
||||
{
|
||||
|
|
@ -222,6 +230,8 @@ class GitHubProvider(OAuthProxy):
|
|||
"redirect_path": redirect_path,
|
||||
"required_scopes": required_scopes,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
"resource_server_url": resource_server_url,
|
||||
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
||||
}.items()
|
||||
if v is not NotSet
|
||||
}
|
||||
|
|
@ -242,6 +252,8 @@ class GitHubProvider(OAuthProxy):
|
|||
redirect_path_final = settings.redirect_path or "/auth/callback"
|
||||
timeout_seconds_final = settings.timeout_seconds or 10
|
||||
required_scopes_final = settings.required_scopes or ["user"]
|
||||
resource_server_url_final = settings.resource_server_url or base_url_final
|
||||
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
|
||||
|
||||
# Create GitHub token verifier
|
||||
token_verifier = GitHubTokenVerifier(
|
||||
|
|
@ -264,6 +276,8 @@ class GitHubProvider(OAuthProxy):
|
|||
base_url=base_url_final,
|
||||
redirect_path=redirect_path_final,
|
||||
issuer_url=base_url_final, # We act as the issuer for client registration
|
||||
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
||||
resource_server_url=resource_server_url_final,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ class GoogleProviderSettings(BaseSettings):
|
|||
redirect_path: str | None = None
|
||||
required_scopes: list[str] | None = None
|
||||
timeout_seconds: int | None = None
|
||||
resource_server_url: AnyHttpUrl | str | None = None
|
||||
allowed_client_redirect_uris: list[str] | None = None
|
||||
|
||||
@field_validator("required_scopes", mode="before")
|
||||
@classmethod
|
||||
|
|
@ -215,8 +217,10 @@ class GoogleProvider(OAuthProxy):
|
|||
client_secret: str | NotSetT = NotSet,
|
||||
base_url: AnyHttpUrl | str | NotSetT = NotSet,
|
||||
redirect_path: str | NotSetT = NotSet,
|
||||
required_scopes: list[str] | None | NotSetT = NotSet,
|
||||
required_scopes: list[str] | NotSetT = NotSet,
|
||||
timeout_seconds: int | NotSetT = NotSet,
|
||||
resource_server_url: AnyHttpUrl | str | NotSetT = NotSet,
|
||||
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
||||
):
|
||||
"""Initialize Google OAuth provider.
|
||||
|
||||
|
|
@ -230,6 +234,8 @@ class GoogleProvider(OAuthProxy):
|
|||
- "https://www.googleapis.com/auth/userinfo.email" for email access
|
||||
- "https://www.googleapis.com/auth/userinfo.profile" for profile info
|
||||
timeout_seconds: HTTP request timeout for Google API calls
|
||||
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
||||
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
||||
"""
|
||||
settings = GoogleProviderSettings.model_validate(
|
||||
{
|
||||
|
|
@ -241,6 +247,8 @@ class GoogleProvider(OAuthProxy):
|
|||
"redirect_path": redirect_path,
|
||||
"required_scopes": required_scopes,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
"resource_server_url": resource_server_url,
|
||||
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
||||
}.items()
|
||||
if v is not NotSet
|
||||
}
|
||||
|
|
@ -262,6 +270,8 @@ class GoogleProvider(OAuthProxy):
|
|||
timeout_seconds_final = settings.timeout_seconds or 10
|
||||
# Google requires at least one scope - openid is the minimal OIDC scope
|
||||
required_scopes_final = settings.required_scopes or ["openid"]
|
||||
resource_server_url_final = settings.resource_server_url or base_url_final
|
||||
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
|
||||
|
||||
# Create Google token verifier
|
||||
token_verifier = GoogleTokenVerifier(
|
||||
|
|
@ -284,6 +294,8 @@ class GoogleProvider(OAuthProxy):
|
|||
base_url=base_url_final,
|
||||
redirect_path=redirect_path_final,
|
||||
issuer_url=base_url_final, # We act as the issuer for client registration
|
||||
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
||||
resource_server_url=resource_server_url_final,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ class WorkOSProviderSettings(BaseSettings):
|
|||
redirect_path: str | None = None
|
||||
required_scopes: list[str] | None = None
|
||||
timeout_seconds: int | None = None
|
||||
resource_server_url: AnyHttpUrl | str | None = None
|
||||
allowed_client_redirect_uris: list[str] | None = None
|
||||
|
||||
@field_validator("required_scopes", mode="before")
|
||||
@classmethod
|
||||
|
|
@ -167,6 +169,8 @@ class WorkOSProvider(OAuthProxy):
|
|||
redirect_path: str | NotSetT = NotSet,
|
||||
required_scopes: list[str] | None | NotSetT = NotSet,
|
||||
timeout_seconds: int | NotSetT = NotSet,
|
||||
resource_server_url: AnyHttpUrl | str | NotSetT = NotSet,
|
||||
allowed_client_redirect_uris: list[str] | NotSetT = NotSet,
|
||||
):
|
||||
"""Initialize WorkOS OAuth provider.
|
||||
|
||||
|
|
@ -178,6 +182,10 @@ class WorkOSProvider(OAuthProxy):
|
|||
redirect_path: Redirect path configured in WorkOS (defaults to "/auth/callback")
|
||||
required_scopes: Required OAuth scopes (no default)
|
||||
timeout_seconds: HTTP request timeout for WorkOS API calls
|
||||
resource_server_url: Path of the FastMCP server (defaults to base_url). If your MCP endpoint is at
|
||||
a different path like {base_url}/mcp, specify it here for RFC 8707 compliance.
|
||||
allowed_client_redirect_uris: List of allowed redirect URI patterns for MCP clients.
|
||||
If None (default), all URIs are allowed. If empty list, no URIs are allowed.
|
||||
"""
|
||||
settings = WorkOSProviderSettings.model_validate(
|
||||
{
|
||||
|
|
@ -190,6 +198,8 @@ class WorkOSProvider(OAuthProxy):
|
|||
"redirect_path": redirect_path,
|
||||
"required_scopes": required_scopes,
|
||||
"timeout_seconds": timeout_seconds,
|
||||
"resource_server_url": resource_server_url,
|
||||
"allowed_client_redirect_uris": allowed_client_redirect_uris,
|
||||
}.items()
|
||||
if v is not NotSet
|
||||
}
|
||||
|
|
@ -218,6 +228,8 @@ class WorkOSProvider(OAuthProxy):
|
|||
redirect_path_final = settings.redirect_path or "/auth/callback"
|
||||
timeout_seconds_final = settings.timeout_seconds or 10
|
||||
scopes_final = settings.required_scopes or []
|
||||
resource_server_url_final = settings.resource_server_url or base_url_final
|
||||
allowed_client_redirect_uris_final = settings.allowed_client_redirect_uris
|
||||
|
||||
# Extract secret string from SecretStr
|
||||
client_secret_str = (
|
||||
|
|
@ -241,6 +253,8 @@ class WorkOSProvider(OAuthProxy):
|
|||
base_url=base_url_final,
|
||||
redirect_path=redirect_path_final,
|
||||
issuer_url=base_url_final,
|
||||
allowed_client_redirect_uris=allowed_client_redirect_uris_final,
|
||||
resource_server_url=resource_server_url_final,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -33,8 +33,9 @@ def validate_redirect_uri(
|
|||
|
||||
Args:
|
||||
redirect_uri: The redirect URI to validate
|
||||
allowed_patterns: List of allowed patterns. If None, defaults to localhost.
|
||||
If empty list, all URIs are allowed.
|
||||
allowed_patterns: List of allowed patterns. If None, all URIs are allowed (for DCR compatibility).
|
||||
If empty list, no URIs are allowed.
|
||||
To restrict to localhost only, explicitly pass DEFAULT_LOCALHOST_PATTERNS.
|
||||
|
||||
Returns:
|
||||
True if the redirect URI is allowed
|
||||
|
|
@ -44,15 +45,9 @@ def validate_redirect_uri(
|
|||
|
||||
uri_str = str(redirect_uri)
|
||||
|
||||
# If no patterns specified, default to localhost only
|
||||
# If no patterns specified, allow all for DCR compatibility
|
||||
# (clients need to dynamically register with their own redirect URIs)
|
||||
if allowed_patterns is None:
|
||||
allowed_patterns = [
|
||||
"http://localhost:*",
|
||||
"http://127.0.0.1:*",
|
||||
]
|
||||
|
||||
# Empty list means allow all
|
||||
if len(allowed_patterns) == 0:
|
||||
return True
|
||||
|
||||
# Check if URI matches any allowed pattern
|
||||
|
|
|
|||
|
|
@ -2,11 +2,20 @@
|
|||
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
from logging import Logger
|
||||
from typing import Any
|
||||
|
||||
import pydantic_core
|
||||
|
||||
from .middleware import CallNext, Middleware, MiddlewareContext
|
||||
|
||||
|
||||
def default_serializer(data: Any) -> str:
|
||||
"""The default serializer for Payloads in the logging middleware."""
|
||||
return pydantic_core.to_json(data, fallback=str).decode()
|
||||
|
||||
|
||||
class LoggingMiddleware(Middleware):
|
||||
"""Middleware that provides comprehensive request and response logging.
|
||||
|
||||
|
|
@ -33,6 +42,7 @@ class LoggingMiddleware(Middleware):
|
|||
include_payloads: bool = False,
|
||||
max_payload_length: int = 1000,
|
||||
methods: list[str] | None = None,
|
||||
payload_serializer: Callable[[Any], str] | None = None,
|
||||
):
|
||||
"""Initialize logging middleware.
|
||||
|
||||
|
|
@ -43,13 +53,14 @@ class LoggingMiddleware(Middleware):
|
|||
max_payload_length: Maximum length of payload to log (prevents huge logs)
|
||||
methods: List of methods to log. If None, logs all methods.
|
||||
"""
|
||||
self.logger = logger or logging.getLogger("fastmcp.requests")
|
||||
self.log_level = log_level
|
||||
self.include_payloads = include_payloads
|
||||
self.max_payload_length = max_payload_length
|
||||
self.methods = methods
|
||||
self.logger: Logger = logger or logging.getLogger("fastmcp.requests")
|
||||
self.log_level: int = log_level
|
||||
self.include_payloads: bool = include_payloads
|
||||
self.max_payload_length: int = max_payload_length
|
||||
self.methods: list[str] | None = methods
|
||||
self.payload_serializer: Callable[[Any], str] | None = payload_serializer
|
||||
|
||||
def _format_message(self, context: MiddlewareContext) -> str:
|
||||
def _format_message(self, context: MiddlewareContext[Any]) -> str:
|
||||
"""Format a message for logging."""
|
||||
parts = [
|
||||
f"source={context.source}",
|
||||
|
|
@ -57,18 +68,29 @@ class LoggingMiddleware(Middleware):
|
|||
f"method={context.method or 'unknown'}",
|
||||
]
|
||||
|
||||
if self.include_payloads and hasattr(context.message, "__dict__"):
|
||||
try:
|
||||
payload = json.dumps(context.message.__dict__, default=str)
|
||||
if len(payload) > self.max_payload_length:
|
||||
payload = payload[: self.max_payload_length] + "..."
|
||||
parts.append(f"payload={payload}")
|
||||
except (TypeError, ValueError):
|
||||
parts.append("payload=<non-serializable>")
|
||||
if self.include_payloads:
|
||||
payload: str
|
||||
|
||||
if not self.payload_serializer:
|
||||
payload = default_serializer(context.message)
|
||||
else:
|
||||
try:
|
||||
payload = self.payload_serializer(context.message)
|
||||
except Exception as e:
|
||||
self.logger.warning(
|
||||
f"Failed {e} to serialize payload: {context.type} {context.method} {context.source}."
|
||||
)
|
||||
payload = default_serializer(context.message)
|
||||
|
||||
if len(payload) > self.max_payload_length:
|
||||
payload = payload[: self.max_payload_length] + "..."
|
||||
|
||||
parts.append(f"payload={payload}")
|
||||
return " ".join(parts)
|
||||
|
||||
async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
|
||||
async def on_message(
|
||||
self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
|
||||
) -> Any:
|
||||
"""Log all messages."""
|
||||
message_info = self._format_message(context)
|
||||
if self.methods and context.method not in self.methods:
|
||||
|
|
@ -111,6 +133,7 @@ class StructuredLoggingMiddleware(Middleware):
|
|||
log_level: int = logging.INFO,
|
||||
include_payloads: bool = False,
|
||||
methods: list[str] | None = None,
|
||||
payload_serializer: Callable[[Any], str] | None = None,
|
||||
):
|
||||
"""Initialize structured logging middleware.
|
||||
|
||||
|
|
@ -119,15 +142,18 @@ class StructuredLoggingMiddleware(Middleware):
|
|||
log_level: Log level for messages (default: INFO)
|
||||
include_payloads: Whether to include message payloads in logs
|
||||
methods: List of methods to log. If None, logs all methods.
|
||||
serializer: Callable that converts objects to a JSON string for the
|
||||
payload. If not provided, uses FastMCP's default tool serializer.
|
||||
"""
|
||||
self.logger = logger or logging.getLogger("fastmcp.structured")
|
||||
self.log_level = log_level
|
||||
self.include_payloads = include_payloads
|
||||
self.methods = methods
|
||||
self.logger: Logger = logger or logging.getLogger("fastmcp.structured")
|
||||
self.log_level: int = log_level
|
||||
self.include_payloads: bool = include_payloads
|
||||
self.methods: list[str] | None = methods
|
||||
self.payload_serializer: Callable[[Any], str] | None = payload_serializer
|
||||
|
||||
def _create_log_entry(
|
||||
self, context: MiddlewareContext, event: str, **extra_fields
|
||||
) -> dict:
|
||||
self, context: MiddlewareContext[Any], event: str, **extra_fields: Any
|
||||
) -> dict[str, Any]:
|
||||
"""Create a structured log entry."""
|
||||
entry = {
|
||||
"event": event,
|
||||
|
|
@ -138,15 +164,27 @@ class StructuredLoggingMiddleware(Middleware):
|
|||
**extra_fields,
|
||||
}
|
||||
|
||||
if self.include_payloads and hasattr(context.message, "__dict__"):
|
||||
try:
|
||||
entry["payload"] = context.message.__dict__
|
||||
except (TypeError, ValueError):
|
||||
entry["payload"] = "<non-serializable>"
|
||||
if self.include_payloads:
|
||||
payload: str
|
||||
|
||||
if not self.payload_serializer:
|
||||
payload = default_serializer(context.message)
|
||||
else:
|
||||
try:
|
||||
payload = self.payload_serializer(context.message)
|
||||
except Exception as e:
|
||||
self.logger.warning(
|
||||
f"Failed {str(e)} to serialize payload: {context.type} {context.method} {context.source}."
|
||||
)
|
||||
payload = default_serializer(context.message)
|
||||
|
||||
entry["payload"] = payload
|
||||
|
||||
return entry
|
||||
|
||||
async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any:
|
||||
async def on_message(
|
||||
self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]
|
||||
) -> Any:
|
||||
"""Log structured message information."""
|
||||
start_entry = self._create_log_entry(context, "request_start")
|
||||
if self.methods and context.method not in self.methods:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from importlib.metadata import version
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
from pydantic import ValidationError
|
||||
from rich.align import Align
|
||||
from rich.console import Console, Group
|
||||
from rich.panel import Panel
|
||||
|
|
@ -10,10 +14,130 @@ from rich.table import Table
|
|||
from rich.text import Text
|
||||
|
||||
import fastmcp
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.mcp_server_config import MCPServerConfig
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
|
||||
from fastmcp.utilities.types import get_cached_typeadapter
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastmcp import FastMCP
|
||||
|
||||
logger = get_logger("cli.config")
|
||||
|
||||
|
||||
def is_already_in_uv_subprocess() -> bool:
|
||||
"""Check if we're already running in a FastMCP uv subprocess."""
|
||||
return bool(os.environ.get("FASTMCP_UV_SPAWNED"))
|
||||
|
||||
|
||||
def load_and_merge_config(
|
||||
server_spec: str | None,
|
||||
**cli_overrides,
|
||||
) -> tuple[MCPServerConfig, str]:
|
||||
"""Load config from server_spec and apply CLI overrides.
|
||||
|
||||
This consolidates the config parsing logic that was duplicated across
|
||||
run, inspect, and dev commands.
|
||||
|
||||
Args:
|
||||
server_spec: Python file, config file, URL, or None to auto-detect
|
||||
cli_overrides: CLI arguments that override config values
|
||||
|
||||
Returns:
|
||||
Tuple of (MCPServerConfig, resolved_server_spec)
|
||||
"""
|
||||
config = None
|
||||
config_path = None
|
||||
|
||||
# Auto-detect fastmcp.json if no server_spec provided
|
||||
if server_spec is None:
|
||||
config_path = Path("fastmcp.json")
|
||||
if not config_path.exists():
|
||||
found_config = MCPServerConfig.find_config()
|
||||
if found_config:
|
||||
config_path = found_config
|
||||
else:
|
||||
logger.error(
|
||||
"No server specification provided and no fastmcp.json found in current directory.\n"
|
||||
"Please specify a server file or create a fastmcp.json configuration."
|
||||
)
|
||||
raise FileNotFoundError("No server specification or fastmcp.json found")
|
||||
|
||||
resolved_spec = str(config_path)
|
||||
logger.info(f"Using configuration from {config_path}")
|
||||
else:
|
||||
resolved_spec = server_spec
|
||||
|
||||
# Load config if server_spec is a .json file
|
||||
if resolved_spec.endswith(".json"):
|
||||
config_path = Path(resolved_spec)
|
||||
if config_path.exists():
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Check if it's an MCPConfig first (has canonical mcpServers key)
|
||||
if "mcpServers" in data:
|
||||
# MCPConfig - we don't process these here, just pass through
|
||||
pass
|
||||
else:
|
||||
# Try to parse as MCPServerConfig
|
||||
try:
|
||||
adapter = get_cached_typeadapter(MCPServerConfig)
|
||||
config = adapter.validate_python(data)
|
||||
|
||||
# Apply deployment settings
|
||||
if config.deployment:
|
||||
config.deployment.apply_runtime_settings(config_path)
|
||||
|
||||
except ValidationError:
|
||||
# Not a valid MCPServerConfig, just pass through
|
||||
pass
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
# Not a valid JSON file, just pass through
|
||||
pass
|
||||
|
||||
# If we don't have a config object yet, create one from filesystem source
|
||||
if config is None:
|
||||
source = FileSystemSource(path=resolved_spec)
|
||||
config = MCPServerConfig(source=source)
|
||||
|
||||
# Convert to dict for immutable transformation
|
||||
config_dict = config.model_dump()
|
||||
|
||||
# Apply CLI overrides to config's environment (always exists due to default_factory)
|
||||
if python_override := cli_overrides.get("python"):
|
||||
config_dict["environment"]["python"] = python_override
|
||||
if packages_override := cli_overrides.get("with_packages"):
|
||||
# Merge packages - CLI packages are added to config packages
|
||||
existing = config_dict["environment"].get("dependencies") or []
|
||||
config_dict["environment"]["dependencies"] = packages_override + existing
|
||||
if requirements_override := cli_overrides.get("with_requirements"):
|
||||
config_dict["environment"]["requirements"] = str(requirements_override)
|
||||
if project_override := cli_overrides.get("project"):
|
||||
config_dict["environment"]["project"] = str(project_override)
|
||||
if editable_override := cli_overrides.get("editable"):
|
||||
config_dict["environment"]["editable"] = editable_override
|
||||
|
||||
# Apply deployment CLI overrides (always exists due to default_factory)
|
||||
if transport_override := cli_overrides.get("transport"):
|
||||
config_dict["deployment"]["transport"] = transport_override
|
||||
if host_override := cli_overrides.get("host"):
|
||||
config_dict["deployment"]["host"] = host_override
|
||||
if port_override := cli_overrides.get("port"):
|
||||
config_dict["deployment"]["port"] = port_override
|
||||
if path_override := cli_overrides.get("path"):
|
||||
config_dict["deployment"]["path"] = path_override
|
||||
if log_level_override := cli_overrides.get("log_level"):
|
||||
config_dict["deployment"]["log_level"] = log_level_override
|
||||
if server_args_override := cli_overrides.get("server_args"):
|
||||
config_dict["deployment"]["args"] = server_args_override
|
||||
|
||||
# Create new config from modified dict
|
||||
new_config = MCPServerConfig(**config_dict)
|
||||
return new_config, resolved_spec
|
||||
|
||||
|
||||
LOGO_ASCII = r"""
|
||||
_ __ ___ _____ __ __ _____________ ____ ____
|
||||
_ __ ___ .'____/___ ______/ /_/ |/ / ____/ __ \ |___ \ / __ \
|
||||
|
|
|
|||
|
|
@ -1,23 +0,0 @@
|
|||
"""FastMCP Configuration module.
|
||||
|
||||
This module provides versioned configuration support for FastMCP servers.
|
||||
The current version is v1, which is re-exported here for convenience.
|
||||
"""
|
||||
|
||||
from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import (
|
||||
Deployment,
|
||||
Environment,
|
||||
FastMCPConfig,
|
||||
generate_schema,
|
||||
)
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.base import BaseSource
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
|
||||
__all__ = [
|
||||
"BaseSource",
|
||||
"Deployment",
|
||||
"Environment",
|
||||
"FastMCPConfig",
|
||||
"FileSystemSource",
|
||||
"generate_schema",
|
||||
]
|
||||
25
src/fastmcp/utilities/mcp_server_config/__init__.py
Normal file
25
src/fastmcp/utilities/mcp_server_config/__init__.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"""FastMCP Configuration module.
|
||||
|
||||
This module provides versioned configuration support for FastMCP servers.
|
||||
The current version is v1, which is re-exported here for convenience.
|
||||
"""
|
||||
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.base import Environment
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
from fastmcp.utilities.mcp_server_config.v1.mcp_server_config import (
|
||||
Deployment,
|
||||
MCPServerConfig,
|
||||
generate_schema,
|
||||
)
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.base import Source
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
|
||||
|
||||
__all__ = [
|
||||
"Source",
|
||||
"Deployment",
|
||||
"Environment",
|
||||
"UVEnvironment",
|
||||
"MCPServerConfig",
|
||||
"FileSystemSource",
|
||||
"generate_schema",
|
||||
]
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
"""Environment configuration for MCP servers."""
|
||||
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.base import Environment
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
|
||||
__all__ = ["Environment", "UVEnvironment"]
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class Environment(BaseModel, ABC):
|
||||
"""Base class for environment configuration."""
|
||||
|
||||
type: str = Field(description="Environment type identifier")
|
||||
|
||||
@abstractmethod
|
||||
def build_command(self, command: list[str]) -> list[str]:
|
||||
"""Build the full command with environment setup.
|
||||
|
||||
Args:
|
||||
command: Base command to wrap with environment setup
|
||||
|
||||
Returns:
|
||||
Full command ready for subprocess execution
|
||||
"""
|
||||
pass
|
||||
|
||||
async def prepare(self, output_dir: Path | None = None) -> None:
|
||||
"""Prepare the environment (optional, can be no-op).
|
||||
|
||||
Args:
|
||||
output_dir: Directory for persistent environment setup
|
||||
"""
|
||||
pass # Default no-op implementation
|
||||
306
src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py
Normal file
306
src/fastmcp/utilities/mcp_server_config/v1/environments/uv.py
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.base import Environment
|
||||
|
||||
logger = get_logger("cli.config")
|
||||
|
||||
|
||||
class UVEnvironment(Environment):
|
||||
"""Configuration for Python environment setup."""
|
||||
|
||||
type: Literal["uv"] = "uv"
|
||||
|
||||
python: str | None = Field(
|
||||
default=None,
|
||||
description="Python version constraint",
|
||||
examples=["3.10", "3.11", "3.12"],
|
||||
)
|
||||
|
||||
dependencies: list[str] | None = Field(
|
||||
default=None,
|
||||
description="Python packages to install with PEP 508 specifiers",
|
||||
examples=[["fastmcp>=2.0,<3", "httpx", "pandas>=2.0"]],
|
||||
)
|
||||
|
||||
requirements: str | None = Field(
|
||||
default=None,
|
||||
description="Path to requirements.txt file",
|
||||
examples=["requirements.txt", "../requirements/prod.txt"],
|
||||
)
|
||||
|
||||
project: str | None = Field(
|
||||
default=None,
|
||||
description="Path to project directory containing pyproject.toml",
|
||||
examples=[".", "../my-project"],
|
||||
)
|
||||
|
||||
editable: list[str] | None = Field(
|
||||
default=None,
|
||||
description="Directories to install in editable mode",
|
||||
examples=[[".", "../my-package"], ["/path/to/package"]],
|
||||
)
|
||||
|
||||
def build_command(self, command: list[str]) -> list[str]:
|
||||
"""Build complete uv run command with environment args and command to execute.
|
||||
|
||||
Args:
|
||||
command: Command to execute (e.g., ["fastmcp", "run", "server.py"])
|
||||
|
||||
Returns:
|
||||
Complete command ready for subprocess.run, including "uv" prefix if needed.
|
||||
If no environment configuration is set, returns the command unchanged.
|
||||
"""
|
||||
# If no environment setup is needed, return command as-is
|
||||
if not self._needs_setup():
|
||||
return command
|
||||
|
||||
args = ["uv", "run"]
|
||||
|
||||
# Add project if specified
|
||||
if self.project:
|
||||
args.extend(["--project", str(self.project)])
|
||||
|
||||
# Add Python version if specified (only if no project, as project has its own Python)
|
||||
if self.python and not self.project:
|
||||
args.extend(["--python", self.python])
|
||||
|
||||
# Always add dependencies, requirements, and editable packages
|
||||
# These work with --project to add additional packages on top of the project env
|
||||
if self.dependencies:
|
||||
for dep in self.dependencies:
|
||||
args.extend(["--with", dep])
|
||||
|
||||
# Add requirements file
|
||||
if self.requirements:
|
||||
args.extend(["--with-requirements", str(self.requirements)])
|
||||
|
||||
# Add editable packages
|
||||
if self.editable:
|
||||
for editable_path in self.editable:
|
||||
args.extend(["--with-editable", str(editable_path)])
|
||||
|
||||
# Add the command
|
||||
args.extend(command)
|
||||
|
||||
return args
|
||||
|
||||
def run_with_uv(self, command: list[str]) -> None:
|
||||
"""Execute a command using uv run with this environment configuration.
|
||||
|
||||
Args:
|
||||
command: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"])
|
||||
"""
|
||||
import subprocess
|
||||
|
||||
# Build the full uv command
|
||||
cmd = self.build_command(command)
|
||||
|
||||
# Set marker to prevent infinite loops when subprocess calls FastMCP again
|
||||
env = os.environ | {"FASTMCP_UV_SPAWNED": "1"}
|
||||
|
||||
logger.debug(f"Running command: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
# Run without capturing output so it flows through naturally
|
||||
process = subprocess.run(cmd, check=True, env=env)
|
||||
sys.exit(process.returncode)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Command failed: {e}")
|
||||
sys.exit(e.returncode)
|
||||
|
||||
def _needs_setup(self) -> bool:
|
||||
"""Check if this environment config requires uv to set up.
|
||||
|
||||
Returns:
|
||||
True if any environment settings require uv run
|
||||
"""
|
||||
return any(
|
||||
[
|
||||
self.python is not None,
|
||||
self.dependencies is not None,
|
||||
self.requirements is not None,
|
||||
self.project is not None,
|
||||
self.editable is not None,
|
||||
]
|
||||
)
|
||||
|
||||
# Backward compatibility aliases
|
||||
def needs_uv(self) -> bool:
|
||||
"""Deprecated: Use _needs_setup() internally or check if build_command modifies the command."""
|
||||
return self._needs_setup()
|
||||
|
||||
def build_uv_run_command(self, command: list[str]) -> list[str]:
|
||||
"""Deprecated: Use build_command() instead."""
|
||||
return self.build_command(command)
|
||||
|
||||
async def prepare(self, output_dir: Path | None = None) -> None:
|
||||
"""Prepare the Python environment using uv.
|
||||
|
||||
Args:
|
||||
output_dir: Directory where the persistent uv project will be created.
|
||||
If None, creates a temporary directory for ephemeral use.
|
||||
"""
|
||||
|
||||
# Check if uv is available
|
||||
if not shutil.which("uv"):
|
||||
raise RuntimeError(
|
||||
"uv is not installed. Please install it with: "
|
||||
"curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||||
)
|
||||
|
||||
# Only prepare environment if there are actual settings to apply
|
||||
if not self._needs_setup():
|
||||
logger.debug("No environment settings configured, skipping preparation")
|
||||
return
|
||||
|
||||
# Handle None case for ephemeral use
|
||||
if output_dir is None:
|
||||
import tempfile
|
||||
|
||||
output_dir = Path(tempfile.mkdtemp(prefix="fastmcp-env-"))
|
||||
logger.info(f"Creating ephemeral environment in {output_dir}")
|
||||
else:
|
||||
logger.info(f"Creating persistent environment in {output_dir}")
|
||||
output_dir = Path(output_dir).resolve()
|
||||
|
||||
# Initialize the project
|
||||
logger.debug(f"Initializing uv project in {output_dir}")
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"uv",
|
||||
"init",
|
||||
"--project",
|
||||
str(output_dir),
|
||||
"--name",
|
||||
"fastmcp-env",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
# If project already exists, that's fine - continue
|
||||
if "already initialized" in e.stderr.lower():
|
||||
logger.debug(
|
||||
f"Project already initialized at {output_dir}, continuing..."
|
||||
)
|
||||
else:
|
||||
logger.error(f"Failed to initialize project: {e.stderr}")
|
||||
raise RuntimeError(f"Failed to initialize project: {e.stderr}") from e
|
||||
|
||||
# Pin Python version if specified
|
||||
if self.python:
|
||||
logger.debug(f"Pinning Python version to {self.python}")
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"uv",
|
||||
"python",
|
||||
"pin",
|
||||
self.python,
|
||||
"--project",
|
||||
str(output_dir),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to pin Python version: {e.stderr}")
|
||||
raise RuntimeError(f"Failed to pin Python version: {e.stderr}") from e
|
||||
|
||||
# Add dependencies with --no-sync to defer installation
|
||||
# dependencies ALWAYS include fastmcp; this is compatible with
|
||||
# specific fastmcp versions that might be in the dependencies list
|
||||
dependencies = (self.dependencies or []) + ["fastmcp"]
|
||||
logger.debug(f"Adding dependencies: {', '.join(dependencies)}")
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"uv",
|
||||
"add",
|
||||
*dependencies,
|
||||
"--no-sync",
|
||||
"--project",
|
||||
str(output_dir),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to add dependencies: {e.stderr}")
|
||||
raise RuntimeError(f"Failed to add dependencies: {e.stderr}") from e
|
||||
|
||||
# Add requirements file if specified
|
||||
if self.requirements:
|
||||
logger.debug(f"Adding requirements from {self.requirements}")
|
||||
# Resolve requirements path relative to current directory
|
||||
req_path = Path(self.requirements).resolve()
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"uv",
|
||||
"add",
|
||||
"-r",
|
||||
str(req_path),
|
||||
"--no-sync",
|
||||
"--project",
|
||||
str(output_dir),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to add requirements: {e.stderr}")
|
||||
raise RuntimeError(f"Failed to add requirements: {e.stderr}") from e
|
||||
|
||||
# Add editable packages if specified
|
||||
if self.editable:
|
||||
editable_paths = [str(Path(e).resolve()) for e in self.editable]
|
||||
logger.debug(f"Adding editable packages: {', '.join(editable_paths)}")
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"uv",
|
||||
"add",
|
||||
"--editable",
|
||||
*editable_paths,
|
||||
"--no-sync",
|
||||
"--project",
|
||||
str(output_dir),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to add editable packages: {e.stderr}")
|
||||
raise RuntimeError(
|
||||
f"Failed to add editable packages: {e.stderr}"
|
||||
) from e
|
||||
|
||||
# Final sync to install everything
|
||||
logger.info("Installing dependencies...")
|
||||
try:
|
||||
subprocess.run(
|
||||
["uv", "sync", "--project", str(output_dir)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to sync dependencies: {e.stderr}")
|
||||
raise RuntimeError(f"Failed to sync dependencies: {e.stderr}") from e
|
||||
|
||||
logger.info(f"Environment prepared successfully in {output_dir}")
|
||||
|
|
@ -10,15 +10,15 @@ from __future__ import annotations
|
|||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal, overload
|
||||
from typing import TYPE_CHECKING, Any, Literal, TypeAlias, cast, overload
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.base import Source
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
|
||||
|
||||
logger = get_logger("cli.config")
|
||||
|
||||
|
|
@ -27,287 +27,10 @@ FASTMCP_JSON_SCHEMA = "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json
|
|||
|
||||
|
||||
# Type alias for source union (will expand with GitSource, etc in future)
|
||||
SourceType = FileSystemSource
|
||||
SourceType: TypeAlias = FileSystemSource
|
||||
|
||||
|
||||
class Environment(BaseModel):
|
||||
"""Configuration for Python environment setup."""
|
||||
|
||||
python: str | None = Field(
|
||||
default=None,
|
||||
description="Python version constraint",
|
||||
examples=["3.10", "3.11", "3.12"],
|
||||
)
|
||||
|
||||
dependencies: list[str] | None = Field(
|
||||
default=None,
|
||||
description="Python packages to install with PEP 508 specifiers",
|
||||
examples=[["fastmcp>=2.0,<3", "httpx", "pandas>=2.0"]],
|
||||
)
|
||||
|
||||
requirements: str | None = Field(
|
||||
default=None,
|
||||
description="Path to requirements.txt file",
|
||||
examples=["requirements.txt", "../requirements/prod.txt"],
|
||||
)
|
||||
|
||||
project: str | None = Field(
|
||||
default=None,
|
||||
description="Path to project directory containing pyproject.toml",
|
||||
examples=[".", "../my-project"],
|
||||
)
|
||||
|
||||
editable: list[str] | None = Field(
|
||||
default=None,
|
||||
description="Directories to install in editable mode",
|
||||
examples=[[".", "../my-package"], ["/path/to/package"]],
|
||||
)
|
||||
|
||||
def build_uv_args(self, command: str | list[str] | None = None) -> list[str]:
|
||||
"""Build uv run arguments from this environment configuration.
|
||||
|
||||
Args:
|
||||
command: Optional command to append (string or list of args)
|
||||
|
||||
Returns:
|
||||
List of arguments for uv run command
|
||||
"""
|
||||
args = ["run"]
|
||||
|
||||
# Add project if specified
|
||||
if self.project:
|
||||
args.extend(["--project", str(self.project)])
|
||||
|
||||
# Add Python version if specified (only if no project, as project has its own Python)
|
||||
if self.python and not self.project:
|
||||
args.extend(["--python", self.python])
|
||||
|
||||
# Always add dependencies, requirements, and editable packages
|
||||
# These work with --project to add additional packages on top of the project env
|
||||
if self.dependencies:
|
||||
for dep in self.dependencies:
|
||||
args.extend(["--with", dep])
|
||||
|
||||
# Add requirements file
|
||||
if self.requirements:
|
||||
args.extend(["--with-requirements", str(self.requirements)])
|
||||
|
||||
# Add editable packages
|
||||
if self.editable:
|
||||
for editable_path in self.editable:
|
||||
args.extend(["--with-editable", str(editable_path)])
|
||||
|
||||
# Add the command if provided
|
||||
if command:
|
||||
if isinstance(command, str):
|
||||
args.append(command)
|
||||
else:
|
||||
args.extend(command)
|
||||
|
||||
return args
|
||||
|
||||
def run_with_uv(self, command: list[str]) -> None:
|
||||
"""Execute a command using uv run with this environment configuration.
|
||||
|
||||
Args:
|
||||
command: Command and arguments to execute (e.g., ["fastmcp", "run", "server.py"])
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Build the full uv command
|
||||
uv_args = self.build_uv_args(command)
|
||||
cmd = ["uv"] + uv_args
|
||||
|
||||
logger.debug(f"Running command: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
# Run without capturing output so it flows through naturally
|
||||
process = subprocess.run(cmd, check=True)
|
||||
sys.exit(process.returncode)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Command failed: {e}")
|
||||
sys.exit(e.returncode)
|
||||
|
||||
def needs_uv(self) -> bool:
|
||||
"""Check if this environment config requires uv to set up.
|
||||
|
||||
Returns:
|
||||
True if any environment settings require uv run
|
||||
"""
|
||||
return any(
|
||||
[
|
||||
self.python is not None,
|
||||
self.dependencies is not None,
|
||||
self.requirements is not None,
|
||||
self.project is not None,
|
||||
self.editable is not None,
|
||||
]
|
||||
)
|
||||
|
||||
async def prepare(self, output_dir: Path | None = None) -> None:
|
||||
"""Prepare the Python environment using uv.
|
||||
|
||||
Args:
|
||||
output_dir: Directory where the persistent uv project will be created.
|
||||
If None, creates a temporary directory for ephemeral use.
|
||||
"""
|
||||
|
||||
# Check if uv is available
|
||||
if not shutil.which("uv"):
|
||||
raise RuntimeError(
|
||||
"uv is not installed. Please install it with: "
|
||||
"curl -LsSf https://astral.sh/uv/install.sh | sh"
|
||||
)
|
||||
|
||||
# Only prepare environment if there are actual settings to apply
|
||||
if not self.needs_uv():
|
||||
logger.debug("No environment settings configured, skipping preparation")
|
||||
return
|
||||
|
||||
# Handle None case for ephemeral use
|
||||
if output_dir is None:
|
||||
import tempfile
|
||||
|
||||
output_dir = Path(tempfile.mkdtemp(prefix="fastmcp-env-"))
|
||||
logger.info(f"Creating ephemeral environment in {output_dir}")
|
||||
else:
|
||||
logger.info(f"Creating persistent environment in {output_dir}")
|
||||
output_dir = Path(output_dir).resolve()
|
||||
|
||||
# Initialize the project
|
||||
logger.debug(f"Initializing uv project in {output_dir}")
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"uv",
|
||||
"init",
|
||||
"--project",
|
||||
str(output_dir),
|
||||
"--name",
|
||||
"fastmcp-env",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
# If project already exists, that's fine - continue
|
||||
if "already initialized" in e.stderr.lower():
|
||||
logger.debug(
|
||||
f"Project already initialized at {output_dir}, continuing..."
|
||||
)
|
||||
else:
|
||||
logger.error(f"Failed to initialize project: {e.stderr}")
|
||||
raise RuntimeError(f"Failed to initialize project: {e.stderr}") from e
|
||||
|
||||
# Pin Python version if specified
|
||||
if self.python:
|
||||
logger.debug(f"Pinning Python version to {self.python}")
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"uv",
|
||||
"python",
|
||||
"pin",
|
||||
self.python,
|
||||
"--project",
|
||||
str(output_dir),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to pin Python version: {e.stderr}")
|
||||
raise RuntimeError(f"Failed to pin Python version: {e.stderr}") from e
|
||||
|
||||
# Add dependencies with --no-sync to defer installation
|
||||
# dependencies ALWAYS include fastmcp; this is compatible with
|
||||
# specific fastmcp versions that might be in the dependencies list
|
||||
dependencies = (self.dependencies or []) + ["fastmcp"]
|
||||
logger.debug(f"Adding dependencies: {', '.join(dependencies)}")
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"uv",
|
||||
"add",
|
||||
*dependencies,
|
||||
"--no-sync",
|
||||
"--project",
|
||||
str(output_dir),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to add dependencies: {e.stderr}")
|
||||
raise RuntimeError(f"Failed to add dependencies: {e.stderr}") from e
|
||||
|
||||
# Add requirements file if specified
|
||||
if self.requirements:
|
||||
logger.debug(f"Adding requirements from {self.requirements}")
|
||||
# Resolve requirements path relative to current directory
|
||||
req_path = Path(self.requirements).resolve()
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"uv",
|
||||
"add",
|
||||
"-r",
|
||||
str(req_path),
|
||||
"--no-sync",
|
||||
"--project",
|
||||
str(output_dir),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to add requirements: {e.stderr}")
|
||||
raise RuntimeError(f"Failed to add requirements: {e.stderr}") from e
|
||||
|
||||
# Add editable packages if specified
|
||||
if self.editable:
|
||||
editable_paths = [str(Path(e).resolve()) for e in self.editable]
|
||||
logger.debug(f"Adding editable packages: {', '.join(editable_paths)}")
|
||||
try:
|
||||
subprocess.run(
|
||||
[
|
||||
"uv",
|
||||
"add",
|
||||
"--editable",
|
||||
*editable_paths,
|
||||
"--no-sync",
|
||||
"--project",
|
||||
str(output_dir),
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to add editable packages: {e.stderr}")
|
||||
raise RuntimeError(
|
||||
f"Failed to add editable packages: {e.stderr}"
|
||||
) from e
|
||||
|
||||
# Final sync to install everything
|
||||
logger.info("Installing dependencies...")
|
||||
try:
|
||||
subprocess.run(
|
||||
["uv", "sync", "--project", str(output_dir)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to sync dependencies: {e.stderr}")
|
||||
raise RuntimeError(f"Failed to sync dependencies: {e.stderr}") from e
|
||||
|
||||
logger.info(f"Environment prepared successfully in {output_dir}")
|
||||
# Type alias for environment union (will expand with other environments in future)
|
||||
EnvironmentType: TypeAlias = UVEnvironment
|
||||
|
||||
|
||||
class Deployment(BaseModel):
|
||||
|
|
@ -408,7 +131,7 @@ class Deployment(BaseModel):
|
|||
return re.sub(r"\$\{([^}]+)\}", replace_var, value)
|
||||
|
||||
|
||||
class FastMCPConfig(BaseModel):
|
||||
class MCPServerConfig(BaseModel):
|
||||
"""Configuration for a FastMCP server.
|
||||
|
||||
This configuration file allows you to specify all settings needed to run
|
||||
|
|
@ -433,8 +156,8 @@ class FastMCPConfig(BaseModel):
|
|||
)
|
||||
|
||||
# Environment configuration
|
||||
environment: Environment = Field(
|
||||
default_factory=lambda: Environment(),
|
||||
environment: EnvironmentType = Field(
|
||||
default_factory=lambda: UVEnvironment(),
|
||||
description="Python environment setup configuration",
|
||||
)
|
||||
|
||||
|
|
@ -450,14 +173,14 @@ class FastMCPConfig(BaseModel):
|
|||
@overload
|
||||
def __init__(self, *, source: dict | FileSystemSource, **data) -> None: ...
|
||||
@overload
|
||||
def __init__(self, *, environment: dict | Environment, **data) -> None: ...
|
||||
def __init__(self, *, environment: dict | UVEnvironment, **data) -> None: ...
|
||||
@overload
|
||||
def __init__(self, *, deployment: dict | Deployment, **data) -> None: ...
|
||||
def __init__(self, **data) -> None: ...
|
||||
|
||||
@field_validator("source", mode="before")
|
||||
@classmethod
|
||||
def validate_source(cls, v: dict | FileSystemSource) -> FileSystemSource:
|
||||
def validate_source(cls, v: dict | Source) -> SourceType:
|
||||
"""Validate and convert source to proper format.
|
||||
|
||||
Supports:
|
||||
|
|
@ -465,34 +188,22 @@ class FastMCPConfig(BaseModel):
|
|||
- FileSystemSource instance (passed through)
|
||||
|
||||
No string parsing happens here - that's only at CLI boundaries.
|
||||
FastMCPConfig works only with properly typed objects.
|
||||
MCPServerConfig works only with properly typed objects.
|
||||
"""
|
||||
if isinstance(v, FileSystemSource):
|
||||
# Already a FileSystemSource instance, return as-is
|
||||
return v
|
||||
elif isinstance(v, dict):
|
||||
# Dict can have type field or not (filesystem is default)
|
||||
if "type" not in v:
|
||||
v["type"] = "filesystem"
|
||||
if isinstance(v, dict):
|
||||
return FileSystemSource(**v)
|
||||
else:
|
||||
raise ValueError("source must be a dict or FileSystemSource instance")
|
||||
return v
|
||||
|
||||
@field_validator("environment", mode="before")
|
||||
@classmethod
|
||||
def validate_environment(cls, v: dict | Environment) -> Environment:
|
||||
"""Validate and convert environment to Environment.
|
||||
def validate_environment(cls, v: dict | Any) -> EnvironmentType:
|
||||
"""Ensure environment has a type field for discrimination.
|
||||
|
||||
Accepts:
|
||||
- Environment instance
|
||||
- dict that can be converted to Environment
|
||||
For backward compatibility, if no type is specified, default to "uv".
|
||||
"""
|
||||
if isinstance(v, Environment):
|
||||
return v
|
||||
elif isinstance(v, dict):
|
||||
return Environment(**v) # type: ignore[arg-type]
|
||||
else:
|
||||
raise ValueError("environment must be a dict, Environment instance")
|
||||
if isinstance(v, dict):
|
||||
return UVEnvironment(**v)
|
||||
return v
|
||||
|
||||
@field_validator("deployment", mode="before")
|
||||
@classmethod
|
||||
|
|
@ -504,22 +215,19 @@ class FastMCPConfig(BaseModel):
|
|||
- dict that can be converted to Deployment
|
||||
|
||||
"""
|
||||
if isinstance(v, Deployment):
|
||||
return v
|
||||
elif isinstance(v, dict):
|
||||
if isinstance(v, dict):
|
||||
return Deployment(**v) # type: ignore[arg-type]
|
||||
else:
|
||||
raise ValueError("deployment must be a dict, Deployment instance")
|
||||
return cast(Deployment, v)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, file_path: Path) -> FastMCPConfig:
|
||||
def from_file(cls, file_path: Path) -> MCPServerConfig:
|
||||
"""Load configuration from a JSON file.
|
||||
|
||||
Args:
|
||||
file_path: Path to the configuration file
|
||||
|
||||
Returns:
|
||||
FastMCPConfig instance
|
||||
MCPServerConfig instance
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the file doesn't exist
|
||||
|
|
@ -552,7 +260,7 @@ class FastMCPConfig(BaseModel):
|
|||
env: dict[str, str] | None = None,
|
||||
cwd: str | None = None,
|
||||
args: list[str] | None = None,
|
||||
) -> FastMCPConfig:
|
||||
) -> MCPServerConfig:
|
||||
"""Create a config from CLI arguments.
|
||||
|
||||
This allows us to have a single code path where everything
|
||||
|
|
@ -575,12 +283,12 @@ class FastMCPConfig(BaseModel):
|
|||
args: Server arguments
|
||||
|
||||
Returns:
|
||||
FastMCPConfig instance
|
||||
MCPServerConfig instance
|
||||
"""
|
||||
# Build environment config if any env args provided
|
||||
environment = None
|
||||
if any([python, dependencies, requirements, project, editable]):
|
||||
environment = Environment(
|
||||
environment = UVEnvironment(
|
||||
python=python,
|
||||
dependencies=dependencies,
|
||||
requirements=requirements,
|
||||
|
|
@ -718,7 +426,7 @@ def generate_schema(output_path: Path | str | None = None) -> dict[str, Any] | N
|
|||
Returns:
|
||||
JSON schema as a dictionary if output_path is None, otherwise None
|
||||
"""
|
||||
schema = FastMCPConfig.model_json_schema()
|
||||
schema = MCPServerConfig.model_json_schema()
|
||||
|
||||
# Add some metadata
|
||||
schema["$id"] = FASTMCP_JSON_SCHEMA
|
||||
|
|
@ -162,9 +162,49 @@
|
|||
"title": "Deployment",
|
||||
"type": "object"
|
||||
},
|
||||
"Environment": {
|
||||
"FileSystemSource": {
|
||||
"description": "Source for local Python files.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "filesystem",
|
||||
"default": "filesystem",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path to Python file containing the server",
|
||||
"title": "Path",
|
||||
"type": "string"
|
||||
},
|
||||
"entrypoint": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
|
||||
"title": "Entrypoint"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"title": "FileSystemSource",
|
||||
"type": "object"
|
||||
},
|
||||
"UVEnvironment": {
|
||||
"description": "Configuration for Python environment setup.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "uv",
|
||||
"default": "uv",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
},
|
||||
"python": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
|
@ -266,42 +306,7 @@
|
|||
"title": "Editable"
|
||||
}
|
||||
},
|
||||
"title": "Environment",
|
||||
"type": "object"
|
||||
},
|
||||
"FileSystemSource": {
|
||||
"description": "Source for local Python files.",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "filesystem",
|
||||
"default": "filesystem",
|
||||
"description": "Source type",
|
||||
"title": "Type",
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"description": "Path to Python file containing the server",
|
||||
"title": "Path",
|
||||
"type": "string"
|
||||
},
|
||||
"entrypoint": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Name of server instance or factory function (a no-arg function that returns a FastMCP server)",
|
||||
"title": "Entrypoint"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"title": "FileSystemSource",
|
||||
"title": "UVEnvironment",
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
|
|
@ -339,7 +344,7 @@
|
|||
]
|
||||
},
|
||||
"environment": {
|
||||
"$ref": "#/$defs/Environment",
|
||||
"$ref": "#/$defs/UVEnvironment",
|
||||
"description": "Python environment setup configuration"
|
||||
},
|
||||
"deployment": {
|
||||
|
|
@ -4,7 +4,7 @@ from typing import Any
|
|||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class BaseSource(BaseModel, ABC):
|
||||
class Source(BaseModel, ABC):
|
||||
"""Abstract base class for all source types."""
|
||||
|
||||
type: str = Field(description="Source type identifier")
|
||||
|
|
@ -6,16 +6,17 @@ from typing import Any, Literal
|
|||
|
||||
from pydantic import Field, field_validator
|
||||
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.base import BaseSource
|
||||
from fastmcp.utilities.logging import get_logger
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.base import Source
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class FileSystemSource(BaseSource):
|
||||
class FileSystemSource(Source):
|
||||
"""Source for local Python files."""
|
||||
|
||||
type: Literal["filesystem"] = Field(default="filesystem", description="Source type")
|
||||
type: Literal["filesystem"] = "filesystem"
|
||||
|
||||
path: str = Field(description="Path to Python file containing the server")
|
||||
entrypoint: str | None = Field(
|
||||
default=None,
|
||||
|
|
@ -458,7 +458,7 @@ class TestWindowsSpecific:
|
|||
"""Test parsing Windows paths with drive letters and colons."""
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import (
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import (
|
||||
FileSystemSource,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ from pathlib import Path
|
|||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import (
|
||||
from fastmcp.utilities.mcp_server_config import (
|
||||
Deployment,
|
||||
Environment,
|
||||
FastMCPConfig,
|
||||
MCPServerConfig,
|
||||
)
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
|
||||
|
||||
|
||||
class TestFileSystemSource:
|
||||
|
|
@ -20,7 +20,7 @@ class TestFileSystemSource:
|
|||
|
||||
def test_dict_source_minimal(self):
|
||||
"""Test that dict source is converted to FileSystemSource."""
|
||||
config = FastMCPConfig(source={"path": "server.py"})
|
||||
config = MCPServerConfig(source={"path": "server.py"})
|
||||
# Dict is converted to FileSystemSource
|
||||
assert isinstance(config.source, FileSystemSource)
|
||||
assert config.source.path == "server.py"
|
||||
|
|
@ -29,7 +29,7 @@ class TestFileSystemSource:
|
|||
|
||||
def test_dict_source_with_entrypoint(self):
|
||||
"""Test dict source with entrypoint field."""
|
||||
config = FastMCPConfig(source={"path": "server.py", "entrypoint": "app"})
|
||||
config = MCPServerConfig(source={"path": "server.py", "entrypoint": "app"})
|
||||
# Dict with entrypoint is converted to FileSystemSource
|
||||
assert isinstance(config.source, FileSystemSource)
|
||||
assert config.source.path == "server.py"
|
||||
|
|
@ -38,7 +38,7 @@ class TestFileSystemSource:
|
|||
|
||||
def test_filesystem_source_entrypoint(self):
|
||||
"""Test FileSystemSource entrypoint format."""
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source=FileSystemSource(path="src/server.py", entrypoint="mcp")
|
||||
)
|
||||
assert isinstance(config.source, FileSystemSource)
|
||||
|
|
@ -52,7 +52,7 @@ class TestEnvironment:
|
|||
|
||||
def test_environment_config_fields(self):
|
||||
"""Test all Environment fields."""
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
environment={
|
||||
"python": "3.12",
|
||||
|
|
@ -73,28 +73,28 @@ class TestEnvironment:
|
|||
def test_needs_uv(self):
|
||||
"""Test needs_uv() method."""
|
||||
# No environment config - doesn't need UV
|
||||
config = FastMCPConfig(source={"path": "server.py"})
|
||||
config = MCPServerConfig(source={"path": "server.py"})
|
||||
assert not config.environment.needs_uv()
|
||||
|
||||
# Empty environment - doesn't need UV
|
||||
config = FastMCPConfig(source={"path": "server.py"}, environment={})
|
||||
config = MCPServerConfig(source={"path": "server.py"}, environment={})
|
||||
assert not config.environment.needs_uv()
|
||||
|
||||
# With dependencies - needs UV
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"}, environment={"dependencies": ["requests"]}
|
||||
)
|
||||
assert config.environment.needs_uv()
|
||||
|
||||
# With Python version - needs UV
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"}, environment={"python": "3.12"}
|
||||
)
|
||||
assert config.environment.needs_uv()
|
||||
|
||||
def test_build_uv_args(self):
|
||||
"""Test build_uv_args() method."""
|
||||
config = FastMCPConfig(
|
||||
def test_build_uv_run_command(self):
|
||||
"""Test build_uv_run_command() method."""
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
environment={
|
||||
"python": "3.12",
|
||||
|
|
@ -104,27 +104,28 @@ class TestEnvironment:
|
|||
},
|
||||
)
|
||||
|
||||
args = config.environment.build_uv_args(["fastmcp", "run", "server.py"])
|
||||
cmd = config.environment.build_command(["fastmcp", "run", "server.py"])
|
||||
|
||||
assert args[0] == "run"
|
||||
assert cmd[0] == "uv"
|
||||
assert cmd[1] == "run"
|
||||
# Python version not added when project is specified (project defines its own Python)
|
||||
assert "--python" not in args
|
||||
assert "3.12" not in args
|
||||
assert "--project" in args
|
||||
assert "." in args
|
||||
assert "--with" in args
|
||||
assert "requests" in args
|
||||
assert "numpy" in args
|
||||
assert "--with-requirements" in args
|
||||
assert "requirements.txt" in args
|
||||
assert "--python" not in cmd
|
||||
assert "3.12" not in cmd
|
||||
assert "--project" in cmd
|
||||
assert "." in cmd
|
||||
assert "--with" in cmd
|
||||
assert "requests" in cmd
|
||||
assert "numpy" in cmd
|
||||
assert "--with-requirements" in cmd
|
||||
assert "requirements.txt" in cmd
|
||||
# Command args should be at the end
|
||||
assert "fastmcp" in args[-3:]
|
||||
assert "run" in args[-2:]
|
||||
assert "server.py" in args[-1:]
|
||||
assert "fastmcp" in cmd[-3:]
|
||||
assert "run" in cmd[-2:]
|
||||
assert "server.py" in cmd[-1:]
|
||||
|
||||
def test_run_with_uv(self):
|
||||
"""Test run_with_uv() subprocess execution."""
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"}, environment={"dependencies": ["requests"]}
|
||||
)
|
||||
|
||||
|
|
@ -143,7 +144,7 @@ class TestDeployment:
|
|||
|
||||
def test_deployment_config_fields(self):
|
||||
"""Test all Deployment fields."""
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
deployment={
|
||||
"transport": "http",
|
||||
|
|
@ -175,7 +176,7 @@ class TestDeployment:
|
|||
work_dir = tmp_path / "work"
|
||||
work_dir.mkdir()
|
||||
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
deployment={
|
||||
"env": {"TEST_VAR": "test_value"},
|
||||
|
|
@ -211,7 +212,7 @@ class TestDeployment:
|
|||
os.environ["BASE_URL"] = "example.com"
|
||||
os.environ["ENV_NAME"] = "production"
|
||||
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
deployment={
|
||||
"env": {
|
||||
|
|
@ -252,17 +253,17 @@ class TestDeployment:
|
|||
os.environ[key] = value
|
||||
|
||||
|
||||
class TestFastMCPConfig:
|
||||
"""Test FastMCPConfig root configuration."""
|
||||
class TestMCPServerConfig:
|
||||
"""Test MCPServerConfig root configuration."""
|
||||
|
||||
def test_minimal_config(self):
|
||||
"""Test creating a config with only required fields."""
|
||||
config = FastMCPConfig(source={"path": "server.py"})
|
||||
config = MCPServerConfig(source={"path": "server.py"})
|
||||
assert isinstance(config.source, FileSystemSource)
|
||||
assert config.source.path == "server.py"
|
||||
assert config.source.entrypoint is None
|
||||
# Environment and deployment are now always present but empty
|
||||
assert isinstance(config.environment, Environment)
|
||||
assert isinstance(config.environment, UVEnvironment)
|
||||
assert isinstance(config.deployment, Deployment)
|
||||
# Check they have no values set
|
||||
assert not config.environment.needs_uv()
|
||||
|
|
@ -273,7 +274,7 @@ class TestFastMCPConfig:
|
|||
|
||||
def test_nested_structure(self):
|
||||
"""Test the nested configuration structure."""
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
environment={
|
||||
"python": "3.12",
|
||||
|
|
@ -288,7 +289,7 @@ class TestFastMCPConfig:
|
|||
assert isinstance(config.source, FileSystemSource)
|
||||
assert config.source.path == "server.py"
|
||||
assert config.source.entrypoint is None
|
||||
assert isinstance(config.environment, Environment)
|
||||
assert isinstance(config.environment, UVEnvironment)
|
||||
assert isinstance(config.deployment, Deployment)
|
||||
|
||||
def test_from_file(self, tmp_path):
|
||||
|
|
@ -303,7 +304,7 @@ class TestFastMCPConfig:
|
|||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
config = FastMCPConfig.from_file(config_file)
|
||||
config = MCPServerConfig.from_file(config_file)
|
||||
|
||||
# When loaded from JSON with entrypoint format, it becomes EntrypointConfig
|
||||
assert isinstance(config.source, FileSystemSource)
|
||||
|
|
@ -324,7 +325,7 @@ class TestFastMCPConfig:
|
|||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
config = FastMCPConfig.from_file(config_file)
|
||||
config = MCPServerConfig.from_file(config_file)
|
||||
# String entrypoint with : should be converted to EntrypointConfig
|
||||
assert isinstance(config.source, FileSystemSource)
|
||||
assert config.source.path == "server.py"
|
||||
|
|
@ -341,7 +342,7 @@ class TestFastMCPConfig:
|
|||
config_file = tmp_path / "fastmcp.json"
|
||||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
config = FastMCPConfig.from_file(config_file)
|
||||
config = MCPServerConfig.from_file(config_file)
|
||||
|
||||
# Should be parsed into EntrypointConfig
|
||||
assert isinstance(config.source, FileSystemSource)
|
||||
|
|
@ -364,7 +365,7 @@ class TestFastMCPConfig:
|
|||
original_cwd = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmp_path)
|
||||
found = FastMCPConfig.find_config()
|
||||
found = MCPServerConfig.find_config()
|
||||
assert found == config_file
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
|
@ -378,7 +379,7 @@ class TestFastMCPConfig:
|
|||
subdir.mkdir()
|
||||
|
||||
# Should NOT find config in parent directory
|
||||
found = FastMCPConfig.find_config(subdir)
|
||||
found = MCPServerConfig.find_config(subdir)
|
||||
assert found is None
|
||||
|
||||
def test_find_config_in_specified_dir(self, tmp_path):
|
||||
|
|
@ -387,12 +388,12 @@ class TestFastMCPConfig:
|
|||
config_file.write_text(json.dumps({"source": {"path": "server.py"}}))
|
||||
|
||||
# Should find config when looking in the directory that contains it
|
||||
found = FastMCPConfig.find_config(tmp_path)
|
||||
found = MCPServerConfig.find_config(tmp_path)
|
||||
assert found == config_file
|
||||
|
||||
def test_find_config_not_found(self, tmp_path):
|
||||
"""Test when config is not found."""
|
||||
found = FastMCPConfig.find_config(tmp_path)
|
||||
found = MCPServerConfig.find_config(tmp_path)
|
||||
assert found is None
|
||||
|
||||
def test_invalid_transport(self, tmp_path):
|
||||
|
|
@ -406,20 +407,20 @@ class TestFastMCPConfig:
|
|||
config_file.write_text(json.dumps(config_data))
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
FastMCPConfig.from_file(config_file)
|
||||
MCPServerConfig.from_file(config_file)
|
||||
|
||||
def test_optional_sections(self):
|
||||
"""Test that all config sections are optional except source."""
|
||||
# Only source is required
|
||||
config = FastMCPConfig(source={"path": "server.py"})
|
||||
config = MCPServerConfig(source={"path": "server.py"})
|
||||
assert isinstance(config.source, FileSystemSource)
|
||||
assert config.source.path == "server.py"
|
||||
# Environment and deployment are now always present but may be empty
|
||||
assert isinstance(config.environment, Environment)
|
||||
assert isinstance(config.environment, UVEnvironment)
|
||||
assert isinstance(config.deployment, Deployment)
|
||||
|
||||
# Only environment with values
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"}, environment={"python": "3.12"}
|
||||
)
|
||||
assert config.environment.python == "3.12"
|
||||
|
|
@ -430,12 +431,14 @@ class TestFastMCPConfig:
|
|||
)
|
||||
|
||||
# Only deployment with values
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"}, deployment={"transport": "http"}
|
||||
)
|
||||
assert isinstance(config.environment, Environment)
|
||||
assert isinstance(config.environment, UVEnvironment)
|
||||
# Check all fields except 'type' which has a default value
|
||||
assert all(
|
||||
getattr(config.environment, field, None) is None
|
||||
for field in Environment.model_fields
|
||||
for field in UVEnvironment.model_fields
|
||||
if field != "type"
|
||||
)
|
||||
assert config.deployment.transport == "http"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ from pathlib import Path
|
|||
import pytest
|
||||
|
||||
from fastmcp.client import Client
|
||||
from fastmcp.utilities.fastmcp_config import FastMCPConfig
|
||||
from fastmcp.utilities.mcp_server_config import MCPServerConfig
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -89,7 +89,7 @@ class TestConfigWithClient:
|
|||
"""Test that a server loaded from config works with a client."""
|
||||
# Load the config
|
||||
config_file = server_with_config / "fastmcp.json"
|
||||
config = FastMCPConfig.from_file(config_file)
|
||||
config = MCPServerConfig.from_file(config_file)
|
||||
|
||||
# Import the server using the source
|
||||
import importlib.util
|
||||
|
|
@ -132,7 +132,7 @@ class TestEnvironmentExecution:
|
|||
|
||||
def test_needs_uv_with_dependencies(self):
|
||||
"""Test that environment with dependencies needs UV."""
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
environment={"dependencies": ["requests", "numpy"]}, # type: ignore[arg-type]
|
||||
)
|
||||
|
|
@ -142,7 +142,7 @@ class TestEnvironmentExecution:
|
|||
|
||||
def test_needs_uv_with_python_version(self):
|
||||
"""Test that environment with Python version needs UV."""
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
environment={"python": "3.12"}, # type: ignore[arg-type]
|
||||
)
|
||||
|
|
@ -152,7 +152,7 @@ class TestEnvironmentExecution:
|
|||
|
||||
def test_no_uv_needed_without_environment(self):
|
||||
"""Test that no UV is needed without environment config."""
|
||||
config = FastMCPConfig(source={"path": "server.py"})
|
||||
config = MCPServerConfig(source={"path": "server.py"})
|
||||
|
||||
# Environment is now always present but may be empty
|
||||
assert config.environment is not None
|
||||
|
|
@ -160,7 +160,7 @@ class TestEnvironmentExecution:
|
|||
|
||||
def test_no_uv_needed_with_empty_environment(self):
|
||||
"""Test that no UV is needed with empty environment config."""
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
environment={}, # type: ignore[arg-type]
|
||||
)
|
||||
|
|
@ -184,7 +184,7 @@ class TestPathResolution:
|
|||
server_file = src_dir / "server.py"
|
||||
server_file.write_text("# Server")
|
||||
|
||||
config = FastMCPConfig(source={"path": "../src/server.py"})
|
||||
config = MCPServerConfig(source={"path": "../src/server.py"})
|
||||
|
||||
# The source path is resolved during load_server
|
||||
# For now, just check that the source is created correctly
|
||||
|
|
@ -198,7 +198,7 @@ class TestPathResolution:
|
|||
work_dir = tmp_path / "work"
|
||||
work_dir.mkdir()
|
||||
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
deployment={"cwd": "work"}, # type: ignore[arg-type]
|
||||
)
|
||||
|
|
@ -222,19 +222,19 @@ class TestPathResolution:
|
|||
reqs_file = tmp_path / "requirements.txt"
|
||||
reqs_file.write_text("fastmcp>=2.0")
|
||||
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
environment={"requirements": "requirements.txt"}, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
# Build UV args
|
||||
# Build UV command
|
||||
assert config.environment is not None
|
||||
uv_args = config.environment.build_uv_args(["fastmcp", "run"])
|
||||
uv_cmd = config.environment.build_command(["fastmcp", "run"])
|
||||
|
||||
# Should include requirements file
|
||||
assert "--with-requirements" in uv_args
|
||||
req_idx = uv_args.index("--with-requirements") + 1
|
||||
assert uv_args[req_idx] == "requirements.txt"
|
||||
assert "--with-requirements" in uv_cmd
|
||||
req_idx = uv_cmd.index("--with-requirements") + 1
|
||||
assert uv_cmd[req_idx] == "requirements.txt"
|
||||
|
||||
|
||||
class TestConfigValidation:
|
||||
|
|
@ -243,7 +243,7 @@ class TestConfigValidation:
|
|||
def test_invalid_transport_rejected(self):
|
||||
"""Test that invalid transport values are rejected."""
|
||||
with pytest.raises(ValueError):
|
||||
FastMCPConfig(
|
||||
MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
deployment={"transport": "invalid_transport"}, # type: ignore[arg-type]
|
||||
)
|
||||
|
|
@ -251,7 +251,7 @@ class TestConfigValidation:
|
|||
def test_streamable_http_transport_rejected(self):
|
||||
"""Test that streamable-http transport is rejected in fastmcp.json config."""
|
||||
with pytest.raises(ValueError):
|
||||
FastMCPConfig(
|
||||
MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
deployment={"transport": "streamable-http"}, # type: ignore[arg-type]
|
||||
)
|
||||
|
|
@ -259,7 +259,7 @@ class TestConfigValidation:
|
|||
def test_invalid_log_level_rejected(self):
|
||||
"""Test that invalid log level values are rejected."""
|
||||
with pytest.raises(ValueError):
|
||||
FastMCPConfig(
|
||||
MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
deployment={"log_level": "INVALID"}, # type: ignore[arg-type]
|
||||
)
|
||||
|
|
@ -267,12 +267,12 @@ class TestConfigValidation:
|
|||
def test_missing_source_rejected(self):
|
||||
"""Test that config without source is rejected."""
|
||||
with pytest.raises(ValueError):
|
||||
FastMCPConfig() # type: ignore[call-arg]
|
||||
MCPServerConfig() # type: ignore[call-arg]
|
||||
|
||||
def test_valid_transport_values(self):
|
||||
"""Test that all valid transport values are accepted."""
|
||||
for transport in ["stdio", "http", "sse"]:
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
deployment={"transport": transport}, # type: ignore[arg-type]
|
||||
)
|
||||
|
|
@ -282,7 +282,7 @@ class TestConfigValidation:
|
|||
def test_valid_log_levels(self):
|
||||
"""Test that all valid log levels are accepted."""
|
||||
for level in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source={"path": "server.py"},
|
||||
deployment={"log_level": level}, # type: ignore[arg-type]
|
||||
)
|
||||
|
|
@ -1,39 +1,6 @@
|
|||
"""Test that the JSON schema file matches the Pydantic model."""
|
||||
"""Test that the generated JSON schema has the correct structure."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import generate_schema
|
||||
|
||||
|
||||
def test_schema_file_matches_pydantic_model():
|
||||
"""Test that the schema.json file matches what the Pydantic model generates."""
|
||||
# Path to the schema file
|
||||
schema_file = (
|
||||
Path(__file__).parent.parent.parent
|
||||
/ "src"
|
||||
/ "fastmcp"
|
||||
/ "utilities"
|
||||
/ "fastmcp_config"
|
||||
/ "v1"
|
||||
/ "schema.json"
|
||||
)
|
||||
|
||||
# Load the schema file
|
||||
with open(schema_file) as f:
|
||||
file_schema = json.load(f)
|
||||
|
||||
# Generate schema from Pydantic model
|
||||
generated_schema = generate_schema()
|
||||
|
||||
# They should be identical
|
||||
assert file_schema == generated_schema, (
|
||||
"The schema.json file does not match the Pydantic model schema. "
|
||||
"Please regenerate the schema file by running:\n"
|
||||
'uv run python -c "from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import generate_schema; '
|
||||
'import json; print(json.dumps(generate_schema(), indent=2))" > '
|
||||
f"{schema_file}"
|
||||
)
|
||||
from fastmcp.utilities.mcp_server_config.v1.mcp_server_config import generate_schema
|
||||
|
||||
|
||||
def test_schema_has_correct_id():
|
||||
|
|
@ -72,8 +39,22 @@ def test_schema_nested_structure():
|
|||
# Check environment section
|
||||
assert "environment" in properties
|
||||
env_schema = properties["environment"]
|
||||
if "properties" in env_schema:
|
||||
# Environment can be in anyOf or direct properties
|
||||
if "anyOf" in env_schema:
|
||||
# Find the UVEnvironment in anyOf
|
||||
for option in env_schema["anyOf"]:
|
||||
if option.get("type") == "object" and "properties" in option:
|
||||
env_props = option["properties"]
|
||||
assert "type" in env_props # New type field
|
||||
assert "python" in env_props
|
||||
assert "dependencies" in env_props
|
||||
assert "requirements" in env_props
|
||||
assert "project" in env_props
|
||||
assert "editable" in env_props
|
||||
break
|
||||
elif "properties" in env_schema:
|
||||
env_props = env_schema["properties"]
|
||||
assert "type" in env_props # New type field
|
||||
assert "python" in env_props
|
||||
assert "dependencies" in env_props
|
||||
assert "requirements" in env_props
|
||||
|
|
@ -6,26 +6,27 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|||
|
||||
import pytest
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import Environment, FastMCPConfig
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
from fastmcp.utilities.mcp_server_config import MCPServerConfig
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
|
||||
|
||||
|
||||
class TestFastMCPConfigPrepare:
|
||||
"""Test the FastMCPConfig.prepare() method."""
|
||||
class TestMCPServerConfigPrepare:
|
||||
"""Test the MCPServerConfig.prepare() method."""
|
||||
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.FastMCPConfig.prepare_source",
|
||||
"fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.FastMCPConfig.prepare_environment",
|
||||
"fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_environment",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
async def test_prepare_calls_both_methods(self, mock_env, mock_src):
|
||||
"""Test that prepare() calls both prepare_environment and prepare_source."""
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source=FileSystemSource(path="server.py"),
|
||||
environment=Environment(python="3.10"),
|
||||
environment=UVEnvironment(python="3.10"),
|
||||
)
|
||||
|
||||
await config.prepare()
|
||||
|
|
@ -34,18 +35,18 @@ class TestFastMCPConfigPrepare:
|
|||
mock_src.assert_called_once()
|
||||
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.FastMCPConfig.prepare_source",
|
||||
"fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.FastMCPConfig.prepare_environment",
|
||||
"fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_environment",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
async def test_prepare_with_output_dir(self, mock_env, mock_src):
|
||||
"""Test that prepare() with output_dir calls prepare_environment with it."""
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source=FileSystemSource(path="server.py"),
|
||||
environment=Environment(python="3.10"),
|
||||
environment=UVEnvironment(python="3.10"),
|
||||
)
|
||||
|
||||
output_path = Path("/tmp/test-env")
|
||||
|
|
@ -55,18 +56,18 @@ class TestFastMCPConfigPrepare:
|
|||
mock_src.assert_called_once()
|
||||
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.FastMCPConfig.prepare_source",
|
||||
"fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.FastMCPConfig.prepare_environment",
|
||||
"fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_environment",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
async def test_prepare_skip_source(self, mock_env, mock_src):
|
||||
"""Test that prepare() skips source when skip_source=True."""
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source=FileSystemSource(path="server.py"),
|
||||
environment=Environment(python="3.10"),
|
||||
environment=UVEnvironment(python="3.10"),
|
||||
)
|
||||
|
||||
await config.prepare(skip_source=True)
|
||||
|
|
@ -75,16 +76,16 @@ class TestFastMCPConfigPrepare:
|
|||
mock_src.assert_not_called()
|
||||
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.FastMCPConfig.prepare_source",
|
||||
"fastmcp.utilities.mcp_server_config.v1.mcp_server_config.MCPServerConfig.prepare_source",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
@patch(
|
||||
"fastmcp.utilities.fastmcp_config.v1.fastmcp_config.Environment.prepare",
|
||||
"fastmcp.utilities.mcp_server_config.v1.environments.uv.UVEnvironment.prepare",
|
||||
new_callable=AsyncMock,
|
||||
)
|
||||
async def test_prepare_no_environment_settings(self, mock_env_prepare, mock_src):
|
||||
"""Test that prepare() works with default empty environment config."""
|
||||
config = FastMCPConfig(
|
||||
config = MCPServerConfig(
|
||||
source=FileSystemSource(path="server.py"),
|
||||
# environment defaults to empty Environment()
|
||||
)
|
||||
|
|
@ -104,7 +105,7 @@ class TestEnvironmentPrepare:
|
|||
"""Test that prepare() raises error when uv is not installed."""
|
||||
mock_which.return_value = None
|
||||
|
||||
env = Environment(python="3.10")
|
||||
env = UVEnvironment(python="3.10")
|
||||
|
||||
with pytest.raises(RuntimeError, match="uv is not installed"):
|
||||
await env.prepare(tmp_path / "test-env")
|
||||
|
|
@ -115,7 +116,7 @@ class TestEnvironmentPrepare:
|
|||
"""Test that prepare() does nothing when no settings are configured."""
|
||||
mock_which.return_value = "/usr/bin/uv"
|
||||
|
||||
env = Environment() # No settings
|
||||
env = UVEnvironment() # No settings
|
||||
|
||||
await env.prepare(tmp_path / "test-env")
|
||||
|
||||
|
|
@ -131,7 +132,7 @@ class TestEnvironmentPrepare:
|
|||
returncode=0, stdout="Environment cached", stderr=""
|
||||
)
|
||||
|
||||
env = Environment(python="3.10")
|
||||
env = UVEnvironment(python="3.10")
|
||||
|
||||
await env.prepare(tmp_path / "test-env")
|
||||
|
||||
|
|
@ -150,7 +151,7 @@ class TestEnvironmentPrepare:
|
|||
mock_which.return_value = "/usr/bin/uv"
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="", stderr="")
|
||||
|
||||
env = Environment(dependencies=["numpy", "pandas"])
|
||||
env = UVEnvironment(dependencies=["numpy", "pandas"])
|
||||
|
||||
await env.prepare(tmp_path / "test-env")
|
||||
|
||||
|
|
@ -179,7 +180,7 @@ class TestEnvironmentPrepare:
|
|||
1, ["uv"], stderr="Package not found"
|
||||
)
|
||||
|
||||
env = Environment(python="3.10")
|
||||
env = UVEnvironment(python="3.10")
|
||||
|
||||
with pytest.raises(RuntimeError, match="Failed to initialize project"):
|
||||
await env.prepare(tmp_path / "test-env")
|
||||
|
|
@ -188,8 +189,8 @@ class TestEnvironmentPrepare:
|
|||
class TestProjectPrepareCommand:
|
||||
"""Test the CLI project prepare command."""
|
||||
|
||||
@patch("fastmcp.utilities.fastmcp_config.FastMCPConfig.from_file")
|
||||
@patch("fastmcp.utilities.fastmcp_config.FastMCPConfig.find_config")
|
||||
@patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.from_file")
|
||||
@patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.find_config")
|
||||
async def test_project_prepare_auto_detect(self, mock_find, mock_from_file):
|
||||
"""Test project prepare with auto-detected config."""
|
||||
from fastmcp.cli.cli import prepare
|
||||
|
|
@ -220,7 +221,7 @@ class TestProjectPrepareCommand:
|
|||
assert "Project prepared successfully" in success_call
|
||||
|
||||
@patch("pathlib.Path.exists")
|
||||
@patch("fastmcp.utilities.fastmcp_config.FastMCPConfig.from_file")
|
||||
@patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.from_file")
|
||||
async def test_project_prepare_explicit_path(self, mock_from_file, mock_exists):
|
||||
"""Test project prepare with explicit config path."""
|
||||
from fastmcp.cli.cli import prepare
|
||||
|
|
@ -243,7 +244,7 @@ class TestProjectPrepareCommand:
|
|||
output_dir=Path("./test-env"),
|
||||
)
|
||||
|
||||
@patch("fastmcp.utilities.fastmcp_config.FastMCPConfig.find_config")
|
||||
@patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.find_config")
|
||||
async def test_project_prepare_no_config_found(self, mock_find):
|
||||
"""Test project prepare when no config is found."""
|
||||
from fastmcp.cli.cli import prepare
|
||||
|
|
@ -280,7 +281,7 @@ class TestProjectPrepareCommand:
|
|||
assert "--output-dir parameter is required" in error_msg
|
||||
|
||||
@patch("pathlib.Path.exists")
|
||||
@patch("fastmcp.utilities.fastmcp_config.FastMCPConfig.from_file")
|
||||
@patch("fastmcp.utilities.mcp_server_config.MCPServerConfig.from_file")
|
||||
async def test_project_prepare_failure(self, mock_from_file, mock_exists):
|
||||
"""Test project prepare when prepare() fails."""
|
||||
from fastmcp.cli.cli import prepare
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ from fastmcp.client.client import Client
|
|||
from fastmcp.client.transports import FastMCPTransport
|
||||
from fastmcp.mcp_config import MCPConfig, StdioMCPServer
|
||||
from fastmcp.server.server import FastMCP
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
|
||||
|
||||
|
||||
class TestUrlDetection:
|
||||
|
|
@ -339,7 +339,7 @@ mcp = fastmcp.FastMCP("TestServer")
|
|||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from fastmcp.cli.run import run_command
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import (
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import (
|
||||
FileSystemSource,
|
||||
)
|
||||
|
||||
|
|
@ -368,7 +368,7 @@ mcp = fastmcp.FastMCP("TestServer")
|
|||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from fastmcp.cli.run import run_command
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import (
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import (
|
||||
FileSystemSource,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from fastmcp.cli.run import load_fastmcp_config
|
||||
from fastmcp.utilities.fastmcp_config import (
|
||||
from fastmcp.cli.run import load_mcp_server_config
|
||||
from fastmcp.utilities.mcp_server_config import (
|
||||
Deployment,
|
||||
Environment,
|
||||
FastMCPConfig,
|
||||
MCPServerConfig,
|
||||
)
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -43,20 +43,20 @@ def test_tool(message: str) -> str:
|
|||
return config_file
|
||||
|
||||
|
||||
def test_load_fastmcp_config(sample_config, monkeypatch):
|
||||
def test_load_mcp_server_config(sample_config, monkeypatch):
|
||||
"""Test loading configuration and returning config subsets."""
|
||||
|
||||
# Capture environment changes
|
||||
original_env = dict(os.environ)
|
||||
|
||||
try:
|
||||
config = load_fastmcp_config(sample_config)
|
||||
config = load_mcp_server_config(sample_config)
|
||||
|
||||
# Check that we got the right types
|
||||
assert isinstance(config, FastMCPConfig)
|
||||
assert isinstance(config, MCPServerConfig)
|
||||
assert isinstance(config.source, FileSystemSource)
|
||||
assert isinstance(config.deployment, Deployment)
|
||||
assert isinstance(config.environment, Environment)
|
||||
assert isinstance(config.environment, UVEnvironment)
|
||||
|
||||
# Check source - path is not resolved yet, only during load_server
|
||||
assert config.source.path == "server.py"
|
||||
|
|
@ -95,7 +95,7 @@ def test_load_config_with_entrypoint_source(tmp_path):
|
|||
server_file = src_dir / "server.py"
|
||||
server_file.write_text("# Server")
|
||||
|
||||
config = load_fastmcp_config(config_file)
|
||||
config = load_mcp_server_config(config_file)
|
||||
|
||||
# Check source - path is not resolved yet, only during load_server
|
||||
assert config.source.path == "src/server.py"
|
||||
|
|
@ -125,7 +125,7 @@ def test_load_config_with_cwd(tmp_path):
|
|||
original_cwd = os.getcwd()
|
||||
|
||||
try:
|
||||
config = load_fastmcp_config(config_file) # noqa: F841
|
||||
config = load_mcp_server_config(config_file) # noqa: F841
|
||||
|
||||
# Check that working directory was changed
|
||||
assert Path.cwd() == subdir.resolve()
|
||||
|
|
@ -160,7 +160,7 @@ def test_load_config_with_relative_cwd(tmp_path):
|
|||
original_cwd = os.getcwd()
|
||||
|
||||
try:
|
||||
config = load_fastmcp_config(config_file) # noqa: F841
|
||||
config = load_mcp_server_config(config_file) # noqa: F841
|
||||
|
||||
# Should change to parent directory of config file
|
||||
assert Path.cwd() == subdir1.resolve()
|
||||
|
|
@ -180,7 +180,7 @@ def test_load_minimal_config(tmp_path):
|
|||
server_file = tmp_path / "server.py"
|
||||
server_file.write_text("# Server")
|
||||
|
||||
config = load_fastmcp_config(config_file)
|
||||
config = load_mcp_server_config(config_file)
|
||||
|
||||
# Check we got source - path is not resolved yet, only during load_server
|
||||
assert isinstance(config.source, FileSystemSource)
|
||||
|
|
@ -201,7 +201,7 @@ def test_load_config_with_server_args(tmp_path):
|
|||
server_file = tmp_path / "server.py"
|
||||
server_file.write_text("# Server")
|
||||
|
||||
config = load_fastmcp_config(config_file)
|
||||
config = load_mcp_server_config(config_file)
|
||||
|
||||
assert config.deployment.args == ["--debug", "--config", "custom.json"]
|
||||
|
||||
|
|
@ -221,7 +221,7 @@ def test_config_subset_independence(tmp_path):
|
|||
server_file = tmp_path / "server.py"
|
||||
server_file.write_text("# Server")
|
||||
|
||||
config = load_fastmcp_config(config_file)
|
||||
config = load_mcp_server_config(config_file)
|
||||
|
||||
# Each subset should be independently usable
|
||||
# Path is not resolved yet, only during load_server
|
||||
|
|
@ -259,15 +259,13 @@ def test_environment_config_path_resolution(tmp_path):
|
|||
server_file = tmp_path / "server.py"
|
||||
server_file.write_text("# Server")
|
||||
|
||||
config = load_fastmcp_config(config_file)
|
||||
config = load_mcp_server_config(config_file)
|
||||
|
||||
# Check that UV args are built with resolved paths
|
||||
uv_args = config.environment.build_uv_args(["fastmcp", "run", "server.py"])
|
||||
# Check that UV command is built with resolved paths
|
||||
uv_cmd = config.environment.build_command(["fastmcp", "run", "server.py"])
|
||||
|
||||
assert "--with-requirements" in uv_args
|
||||
assert "--project" in uv_args
|
||||
assert "--with-requirements" in uv_cmd
|
||||
assert "--project" in uv_cmd
|
||||
# Path should be resolved relative to config file
|
||||
req_idx = uv_args.index("--with-requirements") + 1
|
||||
assert (
|
||||
Path(uv_args[req_idx]).is_absolute() or uv_args[req_idx] == "requirements.txt"
|
||||
)
|
||||
req_idx = uv_cmd.index("--with-requirements") + 1
|
||||
assert Path(uv_cmd[req_idx]).is_absolute() or uv_cmd[req_idx] == "requirements.txt"
|
||||
|
|
|
|||
|
|
@ -25,16 +25,17 @@ class TestRunWithUv:
|
|||
# Check the command that was called
|
||||
mock_run.assert_called_once()
|
||||
cmd = mock_run.call_args[0][0]
|
||||
env = mock_run.call_args.kwargs.get("env", {})
|
||||
|
||||
# With no environment config, the command should be returned unchanged
|
||||
expected = [
|
||||
"uv",
|
||||
"run",
|
||||
"fastmcp",
|
||||
"run",
|
||||
"--skip-env",
|
||||
"server.py",
|
||||
]
|
||||
assert cmd == expected
|
||||
# Check that the environment marker is set
|
||||
assert env.get("FASTMCP_UV_SPAWNED") == "1"
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_run_with_uv_python_version(self, mock_run):
|
||||
|
|
@ -54,7 +55,6 @@ class TestRunWithUv:
|
|||
"3.11",
|
||||
"fastmcp",
|
||||
"run",
|
||||
"--skip-env",
|
||||
"server.py",
|
||||
]
|
||||
assert cmd == expected
|
||||
|
|
@ -80,7 +80,6 @@ class TestRunWithUv:
|
|||
assert cmd[4:] == [
|
||||
"fastmcp",
|
||||
"run",
|
||||
"--skip-env",
|
||||
"server.py",
|
||||
]
|
||||
|
||||
|
|
@ -104,7 +103,6 @@ class TestRunWithUv:
|
|||
"numpy", # original order preserved
|
||||
"fastmcp",
|
||||
"run",
|
||||
"--skip-env",
|
||||
"server.py",
|
||||
]
|
||||
assert cmd == expected
|
||||
|
|
@ -128,7 +126,6 @@ class TestRunWithUv:
|
|||
str(req_path.resolve()), # auto-resolved to absolute path
|
||||
"fastmcp",
|
||||
"run",
|
||||
"--skip-env",
|
||||
"server.py",
|
||||
]
|
||||
assert cmd == expected
|
||||
|
|
@ -152,12 +149,10 @@ class TestRunWithUv:
|
|||
assert exc_info.value.code == 0
|
||||
|
||||
cmd = mock_run.call_args[0][0]
|
||||
# With no environment config, no uv run prefix
|
||||
expected = [
|
||||
"uv",
|
||||
"run",
|
||||
"fastmcp",
|
||||
"run",
|
||||
"--skip-env",
|
||||
"server.py",
|
||||
"--transport",
|
||||
"http",
|
||||
|
|
@ -220,7 +215,6 @@ class TestRunWithUv:
|
|||
assert cmd[next_idx:] == [
|
||||
"fastmcp",
|
||||
"run",
|
||||
"--skip-env",
|
||||
"server.py",
|
||||
"--transport",
|
||||
"http",
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from fastmcp.utilities.fastmcp_config import FastMCPConfig
|
||||
from fastmcp.utilities.fastmcp_config.v1.sources.filesystem import FileSystemSource
|
||||
from fastmcp.utilities.mcp_server_config import MCPServerConfig
|
||||
from fastmcp.utilities.mcp_server_config.v1.sources.filesystem import FileSystemSource
|
||||
|
||||
|
||||
class TestServerArguments:
|
||||
|
|
@ -39,7 +39,7 @@ def get_config() -> dict:
|
|||
|
||||
# Test with arguments
|
||||
source = FileSystemSource(path=str(server_file))
|
||||
config = FastMCPConfig(source=source)
|
||||
config = MCPServerConfig(source=source)
|
||||
|
||||
from fastmcp.cli.cli import with_argv
|
||||
|
||||
|
|
@ -69,7 +69,7 @@ mcp = FastMCP(args.name)
|
|||
""")
|
||||
|
||||
source = FileSystemSource(path=str(server_file))
|
||||
config = FastMCPConfig(source=source)
|
||||
config = MCPServerConfig(source=source)
|
||||
|
||||
from fastmcp.cli.cli import with_argv
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ mcp = FastMCP(name)
|
|||
""")
|
||||
|
||||
source = FileSystemSource(path=str(server_file))
|
||||
config = FastMCPConfig(source=source)
|
||||
config = MCPServerConfig(source=source)
|
||||
|
||||
from fastmcp.cli.cli import with_argv
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ mcp = FastMCP(name)
|
|||
pytest.skip("config_server.py example not found")
|
||||
|
||||
source = FileSystemSource(path=str(config_server))
|
||||
config = FastMCPConfig(source=source)
|
||||
config = MCPServerConfig(source=source)
|
||||
|
||||
from fastmcp.cli.cli import with_argv
|
||||
|
||||
|
|
|
|||
152
tests/client/auth/test_oauth_token_expiry.py
Normal file
152
tests/client/auth/test_oauth_token_expiry.py
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
"""Test OAuth token expiry handling with absolute timestamps."""
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from mcp.shared.auth import OAuthToken
|
||||
|
||||
from fastmcp.client.auth.oauth import FileTokenStorage
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_token_storage_with_expiry(tmp_path: Path):
|
||||
"""Test that tokens are stored with absolute expiry time and loaded correctly."""
|
||||
storage = FileTokenStorage("http://test.example.com", cache_dir=tmp_path)
|
||||
|
||||
# Create a token with 3600 seconds expiry
|
||||
token = OAuthToken(
|
||||
access_token="test_token",
|
||||
token_type="Bearer",
|
||||
expires_in=3600,
|
||||
refresh_token="refresh_token",
|
||||
)
|
||||
|
||||
# Save the token
|
||||
await storage.set_tokens(token)
|
||||
|
||||
# Check that the file contains the dataclass format
|
||||
token_file = storage._get_file_path("tokens")
|
||||
data = json.loads(token_file.read_text())
|
||||
|
||||
assert "token_payload" in data
|
||||
assert "expires_at" in data
|
||||
assert data["expires_at"] is not None
|
||||
# expires_at should be approximately now + 3600 seconds
|
||||
expires_at = datetime.fromisoformat(data["expires_at"].replace("Z", "+00:00"))
|
||||
expected = datetime.now(timezone.utc) + timedelta(seconds=3600)
|
||||
assert abs((expires_at - expected).total_seconds()) < 2
|
||||
|
||||
# Load the token back
|
||||
loaded_token = await storage.get_tokens()
|
||||
assert loaded_token is not None
|
||||
assert loaded_token.access_token == "test_token"
|
||||
# expires_in should be recalculated to be approximately 3600 (minus loading time)
|
||||
assert loaded_token.expires_in is not None
|
||||
assert 3595 <= loaded_token.expires_in <= 3600
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_expired_token_returns_none(tmp_path: Path):
|
||||
"""Test that expired tokens return None when loaded."""
|
||||
storage = FileTokenStorage("http://test.example.com", cache_dir=tmp_path)
|
||||
|
||||
# Manually create an already-expired token file
|
||||
token_file = storage._get_file_path("tokens")
|
||||
past_expiry = datetime.now(timezone.utc) - timedelta(
|
||||
seconds=10
|
||||
) # Expired 10 seconds ago
|
||||
|
||||
expired_token = {
|
||||
"token_payload": {
|
||||
"access_token": "test_token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"refresh_token": "refresh_token",
|
||||
},
|
||||
"expires_at": past_expiry.isoformat(),
|
||||
}
|
||||
token_file.write_text(json.dumps(expired_token, indent=2, default=str))
|
||||
|
||||
# Load the token - should return None since it's expired
|
||||
loaded_token = await storage.get_tokens()
|
||||
assert loaded_token is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_token_without_expiry(tmp_path: Path):
|
||||
"""Test that tokens without expires_in are handled correctly."""
|
||||
storage = FileTokenStorage("http://test.example.com", cache_dir=tmp_path)
|
||||
|
||||
# Create a token without expires_in (perpetual token)
|
||||
token = OAuthToken(
|
||||
access_token="test_token",
|
||||
token_type="Bearer",
|
||||
expires_in=None,
|
||||
refresh_token="refresh_token",
|
||||
)
|
||||
|
||||
# Save the token
|
||||
await storage.set_tokens(token)
|
||||
|
||||
# Check that expires_at is None in the file
|
||||
token_file = storage._get_file_path("tokens")
|
||||
data = json.loads(token_file.read_text())
|
||||
assert data["expires_at"] is None
|
||||
|
||||
# Load the token back - should work since no expiry
|
||||
loaded_token = await storage.get_tokens()
|
||||
assert loaded_token is not None
|
||||
assert loaded_token.access_token == "test_token"
|
||||
assert loaded_token.expires_in is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_format_returns_none(tmp_path: Path):
|
||||
"""Test that invalid token format returns None."""
|
||||
storage = FileTokenStorage("http://test.example.com", cache_dir=tmp_path)
|
||||
|
||||
# Manually write an invalid format token file (missing required fields)
|
||||
token_file = storage._get_file_path("tokens")
|
||||
invalid_token = {
|
||||
"access_token": "invalid_token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"refresh_token": "refresh_token",
|
||||
}
|
||||
token_file.write_text(json.dumps(invalid_token, indent=2))
|
||||
|
||||
# Try to load - should return None
|
||||
loaded_token = await storage.get_tokens()
|
||||
assert loaded_token is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_token_expiry_recalculated_on_load(tmp_path: Path):
|
||||
"""Test that expires_in is correctly recalculated when loading tokens."""
|
||||
storage = FileTokenStorage("http://test.example.com", cache_dir=tmp_path)
|
||||
|
||||
# Manually create a token file with a specific expires_at
|
||||
token_file = storage._get_file_path("tokens")
|
||||
future_expiry = datetime.now(timezone.utc) + timedelta(
|
||||
seconds=1800
|
||||
) # 30 minutes from now
|
||||
|
||||
stored_token = {
|
||||
"token_payload": {
|
||||
"access_token": "test_token",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600, # Original value (will be recalculated)
|
||||
"refresh_token": "refresh_token",
|
||||
},
|
||||
"expires_at": future_expiry.isoformat(),
|
||||
}
|
||||
token_file.write_text(json.dumps(stored_token, indent=2, default=str))
|
||||
|
||||
# Load the token
|
||||
loaded_token = await storage.get_tokens()
|
||||
assert loaded_token is not None
|
||||
# expires_in should be recalculated to approximately 1800 seconds
|
||||
assert loaded_token.expires_in is not None
|
||||
assert 1795 <= loaded_token.expires_in <= 1800
|
||||
|
|
@ -220,6 +220,7 @@ class TestOAuthProxyComprehensive:
|
|||
assert stored_client is not None
|
||||
assert stored_client.client_id == "test-client-id"
|
||||
assert stored_client.client_secret == "test-client-secret"
|
||||
assert stored_client.scope == "read write"
|
||||
|
||||
async def test_register_client_empty_grant_types(self, oauth_proxy):
|
||||
"""Test client registration with empty grant types."""
|
||||
|
|
@ -266,6 +267,7 @@ class TestOAuthProxyComprehensive:
|
|||
assert len(temp_client.redirect_uris) >= 1
|
||||
# ProxyDCRClient uses a placeholder URL but accepts any localhost URI
|
||||
assert str(temp_client.redirect_uris[0]) == "http://localhost/"
|
||||
assert temp_client.scope == "read write"
|
||||
|
||||
# Test that it accepts any localhost redirect URI
|
||||
from pydantic import AnyUrl
|
||||
|
|
@ -379,6 +381,20 @@ class TestOAuthProxyComprehensive:
|
|||
# Proxy should NOT add any scopes - providers handle their own defaults
|
||||
assert "scope" not in query_params
|
||||
|
||||
async def test_client_scope_empty_when_no_required_scopes(self):
|
||||
"""When required_scopes is None/empty, client scope should be empty string."""
|
||||
proxy = OAuthProxy(
|
||||
upstream_authorization_endpoint="https://auth.example.com/authorize",
|
||||
upstream_token_endpoint="https://auth.example.com/token",
|
||||
upstream_client_id="client-123",
|
||||
upstream_client_secret="secret-456",
|
||||
token_verifier=Mock(required_scopes=None),
|
||||
base_url="https://api.example.com",
|
||||
)
|
||||
|
||||
temp_client = await proxy.get_client("any-client")
|
||||
assert temp_client.scope == ""
|
||||
|
||||
async def test_load_authorization_code_valid(self, oauth_proxy):
|
||||
"""Test loading a valid authorization code."""
|
||||
# Store a client code
|
||||
|
|
|
|||
|
|
@ -21,15 +21,15 @@ class MockTokenVerifier(TokenVerifier):
|
|||
class TestProxyDCRClient:
|
||||
"""Test ProxyDCRClient redirect URI validation."""
|
||||
|
||||
def test_default_localhost_only(self):
|
||||
"""Test that default configuration only allows localhost."""
|
||||
def test_default_allows_all(self):
|
||||
"""Test that default configuration allows all URIs for DCR compatibility."""
|
||||
client = ProxyDCRClient(
|
||||
client_id="test",
|
||||
client_secret="secret",
|
||||
redirect_uris=[AnyUrl("http://localhost:3000")],
|
||||
)
|
||||
|
||||
# Localhost should be allowed
|
||||
# All URIs should be allowed by default for DCR compatibility
|
||||
assert client.validate_redirect_uri(AnyUrl("http://localhost:3000")) == AnyUrl(
|
||||
"http://localhost:3000"
|
||||
)
|
||||
|
|
@ -39,11 +39,12 @@ class TestProxyDCRClient:
|
|||
assert client.validate_redirect_uri(AnyUrl("http://127.0.0.1:3000")) == AnyUrl(
|
||||
"http://127.0.0.1:3000"
|
||||
)
|
||||
|
||||
# Non-localhost should fallback to base validation
|
||||
# This will check against registered redirect_uris
|
||||
with pytest.raises(InvalidRedirectUriError):
|
||||
client.validate_redirect_uri(AnyUrl("http://example.com"))
|
||||
assert client.validate_redirect_uri(AnyUrl("http://example.com")) == AnyUrl(
|
||||
"http://example.com"
|
||||
)
|
||||
assert client.validate_redirect_uri(
|
||||
AnyUrl("https://claude.ai/api/mcp/auth_callback")
|
||||
) == AnyUrl("https://claude.ai/api/mcp/auth_callback")
|
||||
|
||||
def test_custom_patterns(self):
|
||||
"""Test custom redirect URI patterns."""
|
||||
|
|
@ -65,8 +66,8 @@ class TestProxyDCRClient:
|
|||
with pytest.raises(InvalidRedirectUriError):
|
||||
client.validate_redirect_uri(AnyUrl("http://127.0.0.1:3000"))
|
||||
|
||||
def test_empty_list_allows_all(self):
|
||||
"""Test that empty pattern list allows all URIs."""
|
||||
def test_empty_list_allows_none(self):
|
||||
"""Test that empty pattern list allows no URIs."""
|
||||
client = ProxyDCRClient(
|
||||
client_id="test",
|
||||
client_secret="secret",
|
||||
|
|
@ -74,10 +75,15 @@ class TestProxyDCRClient:
|
|||
allowed_redirect_uri_patterns=[],
|
||||
)
|
||||
|
||||
# Everything should be allowed
|
||||
# Nothing should be allowed (except the pre-registered one via fallback)
|
||||
# Pre-registered URI should work via fallback to base validation
|
||||
assert client.validate_redirect_uri(AnyUrl("http://localhost:3000"))
|
||||
assert client.validate_redirect_uri(AnyUrl("http://example.com"))
|
||||
assert client.validate_redirect_uri(AnyUrl("https://anywhere.com:9999/path"))
|
||||
|
||||
# Non-registered URIs should be rejected
|
||||
with pytest.raises(InvalidRedirectUriError):
|
||||
client.validate_redirect_uri(AnyUrl("http://example.com"))
|
||||
with pytest.raises(InvalidRedirectUriError):
|
||||
client.validate_redirect_uri(AnyUrl("https://anywhere.com:9999/path"))
|
||||
|
||||
def test_none_redirect_uri(self):
|
||||
"""Test that None redirect URI uses default behavior."""
|
||||
|
|
@ -95,8 +101,8 @@ class TestProxyDCRClient:
|
|||
class TestOAuthProxyRedirectValidation:
|
||||
"""Test OAuth proxy with redirect URI validation."""
|
||||
|
||||
def test_proxy_default_localhost_validation(self):
|
||||
"""Test that OAuth proxy defaults to localhost-only validation."""
|
||||
def test_proxy_default_allows_all(self):
|
||||
"""Test that OAuth proxy defaults to allowing all URIs for DCR compatibility."""
|
||||
proxy = OAuthProxy(
|
||||
upstream_authorization_endpoint="https://auth.example.com/authorize",
|
||||
upstream_token_endpoint="https://auth.example.com/token",
|
||||
|
|
@ -106,7 +112,7 @@ class TestOAuthProxyRedirectValidation:
|
|||
base_url="http://localhost:8000",
|
||||
)
|
||||
|
||||
# The proxy should store None for default localhost patterns
|
||||
# The proxy should store None for default (allow all)
|
||||
assert proxy._allowed_client_redirect_uris is None
|
||||
|
||||
def test_proxy_custom_patterns(self):
|
||||
|
|
@ -126,7 +132,7 @@ class TestOAuthProxyRedirectValidation:
|
|||
assert proxy._allowed_client_redirect_uris == custom_patterns
|
||||
|
||||
def test_proxy_empty_list_validation(self):
|
||||
"""Test OAuth proxy with empty list (allow all)."""
|
||||
"""Test OAuth proxy with empty list (allow none)."""
|
||||
proxy = OAuthProxy(
|
||||
upstream_authorization_endpoint="https://auth.example.com/authorize",
|
||||
upstream_token_endpoint="https://auth.example.com/token",
|
||||
|
|
|
|||
|
|
@ -66,21 +66,20 @@ class TestValidateRedirectUri:
|
|||
assert validate_redirect_uri(None, [])
|
||||
assert validate_redirect_uri(None, ["http://localhost:*"])
|
||||
|
||||
def test_default_localhost_patterns(self):
|
||||
"""Test default localhost-only patterns when None is provided."""
|
||||
# Localhost patterns should be allowed by default
|
||||
def test_default_allows_all(self):
|
||||
"""Test that None (default) allows all URIs for DCR compatibility."""
|
||||
# All URIs should be allowed when None is provided (DCR compatibility)
|
||||
assert validate_redirect_uri("http://localhost:3000", None)
|
||||
assert validate_redirect_uri("http://127.0.0.1:8080", None)
|
||||
assert validate_redirect_uri("http://example.com", None)
|
||||
assert validate_redirect_uri("https://app.example.com", None)
|
||||
assert validate_redirect_uri("https://claude.ai/api/mcp/auth_callback", None)
|
||||
|
||||
# Non-localhost should be rejected by default
|
||||
assert not validate_redirect_uri("http://example.com", None)
|
||||
assert not validate_redirect_uri("https://app.example.com", None)
|
||||
|
||||
def test_empty_list_allows_all(self):
|
||||
"""Test that empty list allows all redirect URIs."""
|
||||
assert validate_redirect_uri("http://localhost:3000", [])
|
||||
assert validate_redirect_uri("http://example.com", [])
|
||||
assert validate_redirect_uri("https://anywhere.com:9999/path", [])
|
||||
def test_empty_list_allows_none(self):
|
||||
"""Test that empty list allows no redirect URIs."""
|
||||
assert not validate_redirect_uri("http://localhost:3000", [])
|
||||
assert not validate_redirect_uri("http://example.com", [])
|
||||
assert not validate_redirect_uri("https://anywhere.com:9999/path", [])
|
||||
|
||||
def test_custom_patterns(self):
|
||||
"""Test validation with custom pattern list."""
|
||||
|
|
@ -122,3 +121,21 @@ class TestDefaultPatterns:
|
|||
"""Test that default patterns include localhost variations."""
|
||||
assert "http://localhost:*" in DEFAULT_LOCALHOST_PATTERNS
|
||||
assert "http://127.0.0.1:*" in DEFAULT_LOCALHOST_PATTERNS
|
||||
|
||||
def test_explicit_localhost_patterns(self):
|
||||
"""Test that explicitly passing DEFAULT_LOCALHOST_PATTERNS restricts to localhost."""
|
||||
# Localhost patterns should be allowed
|
||||
assert validate_redirect_uri(
|
||||
"http://localhost:3000", DEFAULT_LOCALHOST_PATTERNS
|
||||
)
|
||||
assert validate_redirect_uri(
|
||||
"http://127.0.0.1:8080", DEFAULT_LOCALHOST_PATTERNS
|
||||
)
|
||||
|
||||
# Non-localhost should be rejected
|
||||
assert not validate_redirect_uri(
|
||||
"http://example.com", DEFAULT_LOCALHOST_PATTERNS
|
||||
)
|
||||
assert not validate_redirect_uri(
|
||||
"https://claude.ai/api/mcp/auth_callback", DEFAULT_LOCALHOST_PATTERNS
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,30 +1,58 @@
|
|||
"""Tests for logging middleware."""
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Literal, TypeVar
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import mcp
|
||||
import pytest
|
||||
from inline_snapshot import snapshot
|
||||
from pydantic import AnyUrl
|
||||
|
||||
from fastmcp.resources.template import ResourceTemplate
|
||||
from fastmcp.server.middleware.logging import (
|
||||
LoggingMiddleware,
|
||||
StructuredLoggingMiddleware,
|
||||
)
|
||||
from fastmcp.server.middleware.middleware import MiddlewareContext
|
||||
from fastmcp.server.middleware.middleware import CallNext, MiddlewareContext
|
||||
from fastmcp.server.server import FastMCP
|
||||
|
||||
FIXED_DATE = datetime.datetime(2023, 1, 1, tzinfo=datetime.timezone.utc)
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def new_mock_context(
|
||||
message: T,
|
||||
method: str | None = None,
|
||||
source: Literal["server", "client"] | None = None,
|
||||
type: Literal["request", "notification"] | None = None,
|
||||
) -> MiddlewareContext[T]:
|
||||
"""Create a new mock middleware context."""
|
||||
context = MagicMock(spec=MiddlewareContext[T])
|
||||
context.method = method or "test_method"
|
||||
context.source = source or "client"
|
||||
context.type = type or "request"
|
||||
context.message = message
|
||||
context.timestamp = FIXED_DATE
|
||||
return context
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_context():
|
||||
"""Create a mock middleware context."""
|
||||
context = MagicMock(spec=MiddlewareContext)
|
||||
context.method = "test_method"
|
||||
context.source = "client"
|
||||
context.type = "request"
|
||||
context.message = MagicMock()
|
||||
context.message.__dict__ = {"param": "value"}
|
||||
context.timestamp = MagicMock()
|
||||
context.timestamp.isoformat.return_value = "2023-01-01T00:00:00Z"
|
||||
return context
|
||||
|
||||
return new_mock_context(
|
||||
message=mcp.types.CallToolRequest(
|
||||
method="tools/call",
|
||||
params=mcp.types.CallToolRequestParams(
|
||||
name="test_method",
|
||||
arguments={"param": "value"},
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -58,7 +86,9 @@ class TestLoggingMiddleware:
|
|||
assert middleware.include_payloads is True
|
||||
assert middleware.max_payload_length == 500
|
||||
|
||||
def test_format_message_without_payloads(self, mock_context):
|
||||
def test_format_message_without_payloads(
|
||||
self, mock_context: MiddlewareContext[Any]
|
||||
):
|
||||
"""Test message formatting without payloads."""
|
||||
middleware = LoggingMiddleware()
|
||||
formatted = middleware._format_message(mock_context)
|
||||
|
|
@ -68,17 +98,16 @@ class TestLoggingMiddleware:
|
|||
assert "method=test_method" in formatted
|
||||
assert "payload=" not in formatted
|
||||
|
||||
def test_format_message_with_payloads(self, mock_context):
|
||||
def test_format_message_with_payloads(self, mock_context: MiddlewareContext[Any]):
|
||||
"""Test message formatting with payloads."""
|
||||
middleware = LoggingMiddleware(include_payloads=True)
|
||||
formatted = middleware._format_message(mock_context)
|
||||
|
||||
assert "source=client" in formatted
|
||||
assert "type=request" in formatted
|
||||
assert "method=test_method" in formatted
|
||||
assert 'payload={"param": "value"}' in formatted
|
||||
assert formatted == snapshot(
|
||||
'source=client type=request method=test_method payload={"method":"tools/call","params":{"_meta":null,"name":"test_method","arguments":{"param":"value"}}}'
|
||||
)
|
||||
|
||||
def test_format_message_long_payload(self, mock_context):
|
||||
def test_format_message_long_payload(self, mock_context: MiddlewareContext[Any]):
|
||||
"""Test message formatting with long payload truncation."""
|
||||
middleware = LoggingMiddleware(include_payloads=True, max_payload_length=10)
|
||||
formatted = middleware._format_message(mock_context)
|
||||
|
|
@ -86,7 +115,12 @@ class TestLoggingMiddleware:
|
|||
assert "payload=" in formatted
|
||||
assert "..." in formatted
|
||||
|
||||
async def test_on_message_success(self, mock_context, mock_call_next, caplog):
|
||||
async def test_on_message_success(
|
||||
self,
|
||||
mock_context: MiddlewareContext[Any],
|
||||
mock_call_next: CallNext[Any, Any],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
"""Test logging successful messages."""
|
||||
middleware = LoggingMiddleware()
|
||||
|
||||
|
|
@ -98,7 +132,9 @@ class TestLoggingMiddleware:
|
|||
assert "Processing message:" in caplog.text
|
||||
assert "Completed message: test_method" in caplog.text
|
||||
|
||||
async def test_on_message_failure(self, mock_context, caplog):
|
||||
async def test_on_message_failure(
|
||||
self, mock_context: MiddlewareContext[Any], caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""Test logging failed messages."""
|
||||
middleware = LoggingMiddleware()
|
||||
mock_call_next = AsyncMock(side_effect=ValueError("test error"))
|
||||
|
|
@ -121,26 +157,40 @@ class TestStructuredLoggingMiddleware:
|
|||
assert middleware.log_level == logging.INFO
|
||||
assert middleware.include_payloads is False
|
||||
|
||||
def test_create_log_entry_basic(self, mock_context):
|
||||
def test_create_log_entry_basic(self, mock_context: MiddlewareContext[Any]):
|
||||
"""Test creating basic log entry."""
|
||||
middleware = StructuredLoggingMiddleware()
|
||||
entry = middleware._create_log_entry(mock_context, "test_event")
|
||||
|
||||
assert entry["event"] == "test_event"
|
||||
assert entry["timestamp"] == "2023-01-01T00:00:00Z"
|
||||
assert entry["source"] == "client"
|
||||
assert entry["type"] == "request"
|
||||
assert entry["method"] == "test_method"
|
||||
assert "payload" not in entry
|
||||
assert entry == snapshot(
|
||||
{
|
||||
"event": "test_event",
|
||||
"timestamp": "2023-01-01T00:00:00+00:00",
|
||||
"source": "client",
|
||||
"type": "request",
|
||||
"method": "test_method",
|
||||
}
|
||||
)
|
||||
|
||||
def test_create_log_entry_with_payload(self, mock_context):
|
||||
def test_create_log_entry_with_payload(self, mock_context: MiddlewareContext[Any]):
|
||||
"""Test creating log entry with payload."""
|
||||
middleware = StructuredLoggingMiddleware(include_payloads=True)
|
||||
entry = middleware._create_log_entry(mock_context, "test_event")
|
||||
|
||||
assert entry["payload"] == {"param": "value"}
|
||||
assert entry == snapshot(
|
||||
{
|
||||
"event": "test_event",
|
||||
"timestamp": "2023-01-01T00:00:00+00:00",
|
||||
"source": "client",
|
||||
"type": "request",
|
||||
"method": "test_method",
|
||||
"payload": '{"method":"tools/call","params":{"_meta":null,"name":"test_method","arguments":{"param":"value"}}}',
|
||||
}
|
||||
)
|
||||
|
||||
def test_create_log_entry_with_extra_fields(self, mock_context):
|
||||
def test_create_log_entry_with_extra_fields(
|
||||
self, mock_context: MiddlewareContext[Any]
|
||||
):
|
||||
"""Test creating log entry with extra fields."""
|
||||
middleware = StructuredLoggingMiddleware()
|
||||
entry = middleware._create_log_entry(
|
||||
|
|
@ -149,7 +199,12 @@ class TestStructuredLoggingMiddleware:
|
|||
|
||||
assert entry["extra_field"] == "extra_value"
|
||||
|
||||
async def test_on_message_success(self, mock_context, mock_call_next, caplog):
|
||||
async def test_on_message_success(
|
||||
self,
|
||||
mock_context: MiddlewareContext[Any],
|
||||
mock_call_next: CallNext[Any, Any],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
"""Test structured logging of successful messages."""
|
||||
middleware = StructuredLoggingMiddleware()
|
||||
|
||||
|
|
@ -160,17 +215,33 @@ class TestStructuredLoggingMiddleware:
|
|||
|
||||
# Check that we have structured JSON logs
|
||||
log_lines = [record.message for record in caplog.records]
|
||||
|
||||
assert len(log_lines) == 2 # start and success entries
|
||||
|
||||
start_entry = json.loads(log_lines[0])
|
||||
assert start_entry["event"] == "request_start"
|
||||
assert start_entry["method"] == "test_method"
|
||||
assert json.loads(log_lines[0]) == snapshot(
|
||||
{
|
||||
"event": "request_start",
|
||||
"timestamp": "2023-01-01T00:00:00+00:00",
|
||||
"source": "client",
|
||||
"type": "request",
|
||||
"method": "test_method",
|
||||
}
|
||||
)
|
||||
|
||||
success_entry = json.loads(log_lines[1])
|
||||
assert success_entry["event"] == "request_success"
|
||||
assert success_entry["result_type"] == "str"
|
||||
assert json.loads(log_lines[1]) == snapshot(
|
||||
{
|
||||
"event": "request_success",
|
||||
"timestamp": "2023-01-01T00:00:00+00:00",
|
||||
"source": "client",
|
||||
"type": "request",
|
||||
"method": "test_method",
|
||||
"result_type": "str",
|
||||
}
|
||||
)
|
||||
|
||||
async def test_on_message_failure(self, mock_context, caplog):
|
||||
async def test_on_message_failure(
|
||||
self, mock_context: MiddlewareContext[Any], caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""Test structured logging of failed messages."""
|
||||
middleware = StructuredLoggingMiddleware()
|
||||
mock_call_next = AsyncMock(side_effect=ValueError("test error"))
|
||||
|
|
@ -186,10 +257,180 @@ class TestStructuredLoggingMiddleware:
|
|||
start_entry = json.loads(log_lines[0])
|
||||
assert start_entry["event"] == "request_start"
|
||||
|
||||
error_entry = json.loads(log_lines[1])
|
||||
assert error_entry["event"] == "request_error"
|
||||
assert error_entry["error_type"] == "ValueError"
|
||||
assert error_entry["error_message"] == "test error"
|
||||
assert json.loads(log_lines[1]) == snapshot(
|
||||
{
|
||||
"event": "request_error",
|
||||
"timestamp": "2023-01-01T00:00:00+00:00",
|
||||
"source": "client",
|
||||
"type": "request",
|
||||
"method": "test_method",
|
||||
"error_type": "ValueError",
|
||||
"error_message": "test error",
|
||||
}
|
||||
)
|
||||
|
||||
async def test_on_message_with_pydantic_types_in_payload(
|
||||
self,
|
||||
mock_call_next: CallNext[Any, Any],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
"""Ensure Pydantic AnyUrl in payload serializes correctly when include_payloads=True."""
|
||||
|
||||
mock_context = new_mock_context(
|
||||
message=mcp.types.ReadResourceRequest(
|
||||
method="resources/read",
|
||||
params=mcp.types.ReadResourceRequestParams(
|
||||
uri=AnyUrl("test://example/1"),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
middleware = StructuredLoggingMiddleware(include_payloads=True)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
result = await middleware.on_message(mock_context, mock_call_next)
|
||||
|
||||
assert result == "test_result"
|
||||
|
||||
log_lines = [record.message for record in caplog.records]
|
||||
|
||||
assert len(log_lines) == 2
|
||||
assert json.loads(log_lines[0]) == snapshot(
|
||||
{
|
||||
"event": "request_start",
|
||||
"timestamp": "2023-01-01T00:00:00+00:00",
|
||||
"source": "client",
|
||||
"type": "request",
|
||||
"method": "test_method",
|
||||
"payload": '{"method":"resources/read","params":{"_meta":null,"uri":"test://example/1"}}',
|
||||
}
|
||||
)
|
||||
assert json.loads(log_lines[1]) == snapshot(
|
||||
{
|
||||
"event": "request_success",
|
||||
"timestamp": "2023-01-01T00:00:00+00:00",
|
||||
"source": "client",
|
||||
"type": "request",
|
||||
"method": "test_method",
|
||||
"result_type": "str",
|
||||
"payload": '{"method":"resources/read","params":{"_meta":null,"uri":"test://example/1"}}',
|
||||
}
|
||||
)
|
||||
|
||||
async def test_on_message_with_resource_template_in_payload(
|
||||
self,
|
||||
mock_call_next: CallNext[Any, Any],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
):
|
||||
"""Ensure ResourceTemplate in payload serializes via pydantic conversion without errors."""
|
||||
|
||||
mock_context = new_mock_context(
|
||||
message=ResourceTemplate(
|
||||
name="tmpl",
|
||||
uri_template="tmpl://{id}",
|
||||
parameters={"id": {"type": "string"}},
|
||||
)
|
||||
)
|
||||
|
||||
middleware = StructuredLoggingMiddleware(include_payloads=True)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
result = await middleware.on_message(mock_context, mock_call_next)
|
||||
|
||||
assert result == "test_result"
|
||||
|
||||
log_lines = [record.message for record in caplog.records]
|
||||
assert len(log_lines) == 2
|
||||
assert json.loads(log_lines[0]) == snapshot(
|
||||
{
|
||||
"event": "request_start",
|
||||
"timestamp": "2023-01-01T00:00:00+00:00",
|
||||
"source": "client",
|
||||
"type": "request",
|
||||
"method": "test_method",
|
||||
"payload": '{"name":"tmpl","title":null,"description":null,"tags":[],"meta":null,"enabled":true,"uri_template":"tmpl://{id}","mime_type":"text/plain","parameters":{"id":{"type":"string"}},"annotations":null}',
|
||||
}
|
||||
)
|
||||
|
||||
async def test_on_message_with_nonserializable_payload_falls_back_to_str(
|
||||
self, mock_call_next: CallNext[Any, Any], caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""Ensure non-JSONable objects fall back to string serialization in payload."""
|
||||
|
||||
class NonSerializable:
|
||||
def __str__(self) -> str:
|
||||
return "NON_SERIALIZABLE"
|
||||
|
||||
mock_context = new_mock_context(
|
||||
message=mcp.types.CallToolRequest(
|
||||
method="tools/call",
|
||||
params=mcp.types.CallToolRequestParams(
|
||||
name="test_method",
|
||||
arguments={"obj": NonSerializable()},
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
middleware = StructuredLoggingMiddleware(include_payloads=True)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
result = await middleware.on_message(mock_context, mock_call_next)
|
||||
|
||||
assert result == "test_result"
|
||||
|
||||
log_lines = [record.message for record in caplog.records]
|
||||
assert len(log_lines) >= 2
|
||||
assert json.loads(log_lines[0]) == snapshot(
|
||||
{
|
||||
"event": "request_start",
|
||||
"timestamp": "2023-01-01T00:00:00+00:00",
|
||||
"source": "client",
|
||||
"type": "request",
|
||||
"method": "test_method",
|
||||
"payload": '{"method":"tools/call","params":{"_meta":null,"name":"test_method","arguments":{"obj":"NON_SERIALIZABLE"}}}',
|
||||
}
|
||||
)
|
||||
|
||||
async def test_on_message_with_custom_serializer_applied(
|
||||
self, mock_call_next: CallNext[Any, Any], caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""Ensure a custom serializer is used for non-JSONable payloads."""
|
||||
|
||||
# Provide a serializer that replaces entire payload with a fixed string
|
||||
def custom_serializer(_: Any) -> str:
|
||||
return "CUSTOM_PAYLOAD"
|
||||
|
||||
mock_context = new_mock_context(
|
||||
message=mcp.types.CallToolRequest(
|
||||
method="tools/call",
|
||||
params=mcp.types.CallToolRequestParams(
|
||||
name="test_method",
|
||||
arguments={"obj": "OBJECT"},
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
middleware = StructuredLoggingMiddleware(
|
||||
include_payloads=True, payload_serializer=custom_serializer
|
||||
)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
result = await middleware.on_message(mock_context, mock_call_next)
|
||||
|
||||
assert result == "test_result"
|
||||
|
||||
log_lines = [record.message for record in caplog.records]
|
||||
assert len(log_lines) >= 2
|
||||
assert json.loads(log_lines[0]) == snapshot(
|
||||
{
|
||||
"event": "request_start",
|
||||
"timestamp": "2023-01-01T00:00:00+00:00",
|
||||
"source": "client",
|
||||
"type": "request",
|
||||
"method": "test_method",
|
||||
"payload": "CUSTOM_PAYLOAD",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -233,7 +474,7 @@ class TestLoggingMiddlewareIntegration:
|
|||
"""Integration tests for logging middleware with real FastMCP server."""
|
||||
|
||||
async def test_logging_middleware_logs_successful_operations(
|
||||
self, logging_server, caplog
|
||||
self, logging_server: FastMCP, caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""Test that logging middleware captures successful operations."""
|
||||
from fastmcp.client import Client
|
||||
|
|
@ -259,7 +500,9 @@ class TestLoggingMiddlewareIntegration:
|
|||
assert processing_count == 2
|
||||
assert completion_count == 2
|
||||
|
||||
async def test_logging_middleware_logs_failures(self, logging_server, caplog):
|
||||
async def test_logging_middleware_logs_failures(
|
||||
self, logging_server: FastMCP, caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""Test that logging middleware captures failed operations."""
|
||||
from fastmcp.client import Client
|
||||
|
||||
|
|
@ -279,7 +522,9 @@ class TestLoggingMiddlewareIntegration:
|
|||
assert "Processing message:" in log_text
|
||||
assert "Failed message: tools/call" in log_text
|
||||
|
||||
async def test_logging_middleware_with_payloads(self, logging_server, caplog):
|
||||
async def test_logging_middleware_with_payloads(
|
||||
self, logging_server: FastMCP, caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""Test logging middleware when configured to include payloads."""
|
||||
from fastmcp.client import Client
|
||||
|
||||
|
|
@ -300,7 +545,7 @@ class TestLoggingMiddlewareIntegration:
|
|||
assert "payload=" in log_text
|
||||
|
||||
async def test_structured_logging_middleware_produces_json(
|
||||
self, logging_server, caplog
|
||||
self, logging_server: FastMCP, caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""Test that structured logging middleware produces parseable JSON logs."""
|
||||
import json
|
||||
|
|
@ -334,7 +579,7 @@ class TestLoggingMiddlewareIntegration:
|
|||
assert "method" in log_entry
|
||||
|
||||
async def test_structured_logging_middleware_handles_errors(
|
||||
self, logging_server, caplog
|
||||
self, logging_server: FastMCP, caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""Test structured logging of errors with JSON format."""
|
||||
import json
|
||||
|
|
@ -375,7 +620,7 @@ class TestLoggingMiddlewareIntegration:
|
|||
assert "error_message" in error_entry
|
||||
|
||||
async def test_logging_middleware_with_different_operations(
|
||||
self, logging_server, caplog
|
||||
self, logging_server: FastMCP, caplog: pytest.LogCaptureFixture
|
||||
):
|
||||
"""Test logging middleware with various MCP operations."""
|
||||
from fastmcp.client import Client
|
||||
|
|
@ -410,7 +655,9 @@ class TestLoggingMiddlewareIntegration:
|
|||
assert processing_count == 4
|
||||
assert completion_count == 4
|
||||
|
||||
async def test_logging_middleware_custom_configuration(self, logging_server):
|
||||
async def test_logging_middleware_custom_configuration(
|
||||
self, logging_server: FastMCP
|
||||
):
|
||||
"""Test logging middleware with custom logger configuration."""
|
||||
import io
|
||||
import logging
|
||||
|
|
|
|||
|
|
@ -1,24 +1,26 @@
|
|||
"""Tests for CLI utility functions."""
|
||||
|
||||
from fastmcp.utilities.fastmcp_config.v1.fastmcp_config import Environment
|
||||
from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment
|
||||
|
||||
|
||||
class TestEnvironmentBuildUVArgs:
|
||||
"""Test the Environment.build_uv_args() method."""
|
||||
class TestEnvironmentBuildUVRunCommand:
|
||||
"""Test the Environment.build_uv_run_command() method."""
|
||||
|
||||
def test_build_uv_args_basic(self):
|
||||
"""Test building basic uv args with no environment config."""
|
||||
env = Environment()
|
||||
args = env.build_uv_args(["fastmcp", "run", "server.py"])
|
||||
expected = ["run", "fastmcp", "run", "server.py"]
|
||||
assert args == expected
|
||||
def test_build_uv_run_command_basic(self):
|
||||
"""Test building basic uv command with no environment config."""
|
||||
env = UVEnvironment()
|
||||
cmd = env.build_command(["fastmcp", "run", "server.py"])
|
||||
# With no config, the command should be returned unchanged
|
||||
expected = ["fastmcp", "run", "server.py"]
|
||||
assert cmd == expected
|
||||
|
||||
def test_build_uv_args_with_editable(self):
|
||||
"""Test building uv args with editable package."""
|
||||
def test_build_uv_run_command_with_editable(self):
|
||||
"""Test building uv command with editable package."""
|
||||
editable_path = "/path/to/package"
|
||||
env = Environment(editable=[editable_path])
|
||||
args = env.build_uv_args(["fastmcp", "run", "server.py"])
|
||||
env = UVEnvironment(editable=[editable_path])
|
||||
cmd = env.build_command(["fastmcp", "run", "server.py"])
|
||||
expected = [
|
||||
"uv",
|
||||
"run",
|
||||
"--with-editable",
|
||||
editable_path,
|
||||
|
|
@ -26,13 +28,14 @@ class TestEnvironmentBuildUVArgs:
|
|||
"run",
|
||||
"server.py",
|
||||
]
|
||||
assert args == expected
|
||||
assert cmd == expected
|
||||
|
||||
def test_build_uv_args_with_packages(self):
|
||||
"""Test building uv args with additional packages."""
|
||||
env = Environment(dependencies=["pkg1", "pkg2"])
|
||||
args = env.build_uv_args(["fastmcp", "run", "server.py"])
|
||||
def test_build_uv_run_command_with_packages(self):
|
||||
"""Test building uv command with additional packages."""
|
||||
env = UVEnvironment(dependencies=["pkg1", "pkg2"])
|
||||
cmd = env.build_command(["fastmcp", "run", "server.py"])
|
||||
expected = [
|
||||
"uv",
|
||||
"run",
|
||||
"--with",
|
||||
"pkg1",
|
||||
|
|
@ -42,13 +45,14 @@ class TestEnvironmentBuildUVArgs:
|
|||
"run",
|
||||
"server.py",
|
||||
]
|
||||
assert args == expected
|
||||
assert cmd == expected
|
||||
|
||||
def test_build_uv_args_with_python_version(self):
|
||||
"""Test building uv args with Python version."""
|
||||
env = Environment(python="3.10")
|
||||
args = env.build_uv_args(["fastmcp", "run", "server.py"])
|
||||
def test_build_uv_run_command_with_python_version(self):
|
||||
"""Test building uv command with Python version."""
|
||||
env = UVEnvironment(python="3.10")
|
||||
cmd = env.build_command(["fastmcp", "run", "server.py"])
|
||||
expected = [
|
||||
"uv",
|
||||
"run",
|
||||
"--python",
|
||||
"3.10",
|
||||
|
|
@ -56,14 +60,15 @@ class TestEnvironmentBuildUVArgs:
|
|||
"run",
|
||||
"server.py",
|
||||
]
|
||||
assert args == expected
|
||||
assert cmd == expected
|
||||
|
||||
def test_build_uv_args_with_requirements(self):
|
||||
"""Test building uv args with requirements file."""
|
||||
def test_build_uv_run_command_with_requirements(self):
|
||||
"""Test building uv command with requirements file."""
|
||||
requirements_path = "/path/to/requirements.txt"
|
||||
env = Environment(requirements=requirements_path)
|
||||
args = env.build_uv_args(["fastmcp", "run", "server.py"])
|
||||
env = UVEnvironment(requirements=requirements_path)
|
||||
cmd = env.build_command(["fastmcp", "run", "server.py"])
|
||||
expected = [
|
||||
"uv",
|
||||
"run",
|
||||
"--with-requirements",
|
||||
requirements_path,
|
||||
|
|
@ -71,28 +76,37 @@ class TestEnvironmentBuildUVArgs:
|
|||
"run",
|
||||
"server.py",
|
||||
]
|
||||
assert args == expected
|
||||
assert cmd == expected
|
||||
|
||||
def test_build_uv_args_with_project(self):
|
||||
"""Test building uv args with project directory."""
|
||||
def test_build_uv_run_command_with_project(self):
|
||||
"""Test building uv command with project directory."""
|
||||
project_path = "/path/to/project"
|
||||
env = Environment(project=project_path)
|
||||
args = env.build_uv_args(["fastmcp", "run", "server.py"])
|
||||
expected = ["run", "--project", project_path, "fastmcp", "run", "server.py"]
|
||||
assert args == expected
|
||||
env = UVEnvironment(project=project_path)
|
||||
cmd = env.build_command(["fastmcp", "run", "server.py"])
|
||||
expected = [
|
||||
"uv",
|
||||
"run",
|
||||
"--project",
|
||||
project_path,
|
||||
"fastmcp",
|
||||
"run",
|
||||
"server.py",
|
||||
]
|
||||
assert cmd == expected
|
||||
|
||||
def test_build_uv_args_with_everything(self):
|
||||
"""Test building uv args with all options."""
|
||||
def test_build_uv_run_command_with_everything(self):
|
||||
"""Test building uv command with all options."""
|
||||
requirements_path = "/path/to/requirements.txt"
|
||||
editable_path = "/local/pkg"
|
||||
env = Environment(
|
||||
env = UVEnvironment(
|
||||
python="3.10",
|
||||
dependencies=["pandas", "numpy"],
|
||||
requirements=requirements_path,
|
||||
editable=[editable_path],
|
||||
)
|
||||
args = env.build_uv_args(["fastmcp", "run", "server.py"])
|
||||
cmd = env.build_command(["fastmcp", "run", "server.py"])
|
||||
expected = [
|
||||
"uv",
|
||||
"run",
|
||||
"--python",
|
||||
"3.10",
|
||||
|
|
@ -108,33 +122,23 @@ class TestEnvironmentBuildUVArgs:
|
|||
"run",
|
||||
"server.py",
|
||||
]
|
||||
assert args == expected
|
||||
assert cmd == expected
|
||||
|
||||
def test_build_uv_args_no_command(self):
|
||||
"""Test building uv args without command."""
|
||||
env = Environment(dependencies=["pkg1"])
|
||||
args = env.build_uv_args()
|
||||
expected = ["run", "--with", "pkg1"]
|
||||
assert args == expected
|
||||
# Note: These tests are removed because build_uv_run_command now requires a command
|
||||
# and only accepts a list, not optional or string commands
|
||||
|
||||
def test_build_uv_args_with_string_command(self):
|
||||
"""Test building uv args with string command."""
|
||||
env = Environment()
|
||||
args = env.build_uv_args("python")
|
||||
expected = ["run", "python"]
|
||||
assert args == expected
|
||||
|
||||
def test_build_uv_args_project_with_extras(self):
|
||||
def test_build_uv_run_command_project_with_extras(self):
|
||||
"""Test that project flag works with additional dependencies."""
|
||||
project_path = "/path/to/project"
|
||||
env = Environment(
|
||||
env = UVEnvironment(
|
||||
project=project_path,
|
||||
python="3.10", # Should be ignored with project
|
||||
dependencies=["pandas"], # Should be added on top of project
|
||||
editable=["/pkg"], # Should be added on top of project
|
||||
)
|
||||
args = env.build_uv_args(["fastmcp", "run", "server.py"])
|
||||
cmd = env.build_command(["fastmcp", "run", "server.py"])
|
||||
expected = [
|
||||
"uv",
|
||||
"run",
|
||||
"--project",
|
||||
project_path,
|
||||
|
|
@ -146,7 +150,7 @@ class TestEnvironmentBuildUVArgs:
|
|||
"run",
|
||||
"server.py",
|
||||
]
|
||||
assert args == expected
|
||||
assert cmd == expected
|
||||
|
||||
|
||||
class TestEnvironmentNeedsUV:
|
||||
|
|
@ -154,35 +158,35 @@ class TestEnvironmentNeedsUV:
|
|||
|
||||
def test_needs_uv_with_python(self):
|
||||
"""Test that needs_uv returns True with Python version."""
|
||||
env = Environment(python="3.10")
|
||||
env = UVEnvironment(python="3.10")
|
||||
assert env.needs_uv() is True
|
||||
|
||||
def test_needs_uv_with_dependencies(self):
|
||||
"""Test that needs_uv returns True with dependencies."""
|
||||
env = Environment(dependencies=["pandas"])
|
||||
env = UVEnvironment(dependencies=["pandas"])
|
||||
assert env.needs_uv() is True
|
||||
|
||||
def test_needs_uv_with_requirements(self):
|
||||
"""Test that needs_uv returns True with requirements."""
|
||||
env = Environment(requirements="/path/to/requirements.txt")
|
||||
env = UVEnvironment(requirements="/path/to/requirements.txt")
|
||||
assert env.needs_uv() is True
|
||||
|
||||
def test_needs_uv_with_project(self):
|
||||
"""Test that needs_uv returns True with project."""
|
||||
env = Environment(project="/path/to/project")
|
||||
env = UVEnvironment(project="/path/to/project")
|
||||
assert env.needs_uv() is True
|
||||
|
||||
def test_needs_uv_with_editable(self):
|
||||
"""Test that needs_uv returns True with editable."""
|
||||
env = Environment(editable=["/pkg"])
|
||||
env = UVEnvironment(editable=["/pkg"])
|
||||
assert env.needs_uv() is True
|
||||
|
||||
def test_needs_uv_empty(self):
|
||||
"""Test that needs_uv returns False with empty config."""
|
||||
env = Environment()
|
||||
env = UVEnvironment()
|
||||
assert env.needs_uv() is False
|
||||
|
||||
def test_needs_uv_with_empty_lists(self):
|
||||
"""Test that needs_uv returns False with empty lists."""
|
||||
env = Environment(dependencies=None, editable=None)
|
||||
env = UVEnvironment(dependencies=None, editable=None)
|
||||
assert env.needs_uv() is False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue