-
Notifications
You must be signed in to change notification settings - Fork 72
chore(mcp): migrate to fastmcp-extensions library #949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
aaronsteers
merged 18 commits into
main
from
devin/1768864254-migrate-mcp-to-fastmcp-extensions
Jan 20, 2026
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
f424285
refactor(mcp): migrate to fastmcp-extensions library
devin-ai-integration[bot] 07e450a
refactor(mcp): rename modules to preserve original domain names
devin-ai-integration[bot] c586b8b
feat(mcp): add backward-compatible config args and filters for legacy…
devin-ai-integration[bot] 7d27a44
fix(mcp): move type-only imports to TYPE_CHECKING block
devin-ai-integration[bot] 8d77e00
refactor(mcp): use constants for env vars and config names
devin-ai-integration[bot] bc02fa1
fix(mcp): add noqa comments to all side-effect imports
devin-ai-integration[bot] 15b6c39
fix(mcp): remove unnecessary noqa comments from side-effect imports
devin-ai-integration[bot] 2d3509b
refactor(mcp): use explicit registration pattern instead of side-effe…
devin-ai-integration[bot] 45db195
fix(mcp): handle potential None from get_mcp_config() calls
devin-ai-integration[bot] e3f941f
refactor(mcp): delete bin/test_mcp_tool.py in favor of fastmcp-extens…
devin-ai-integration[bot] 07ec4d0
refactor(mcp): delete header extraction functions and use get_mcp_con…
devin-ai-integration[bot] b4721c9
refactor(mcp): move config args and filters from server.py to _tool_u…
devin-ai-integration[bot] d2b9578
refactor(mcp): rename initialize_secrets() to load_secrets_to_env_vars()
devin-ai-integration[bot] 94c9cf9
refactor(mcp): move resolver functions to _arg_resolvers.py
devin-ai-integration[bot] c6f9bc7
refactor(mcp): inline config args in server.py and use multiline list…
devin-ai-integration[bot] 07fcf9b
refactor(mcp): simplify register_cloud_tools by evaluating workspace_…
devin-ai-integration[bot] b12ba50
refactor(mcp): rename _util.py to _config.py
devin-ai-integration[bot] a856bf5
fix(mcp): address CodeRabbit feedback
devin-ai-integration[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,173 @@ | ||
| # Copyright (c) 2025 Airbyte, Inc., all rights reserved. | ||
| """Argument resolver functions for MCP tools. | ||
|
|
||
| This module provides functions to resolve and validate arguments passed to MCP tools, | ||
| including connector configurations and list-of-strings arguments. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| from pathlib import Path | ||
| from typing import Any, overload | ||
|
|
||
| import yaml | ||
|
|
||
| from airbyte.secrets.hydration import deep_update, detect_hardcoded_secrets | ||
| from airbyte.secrets.util import get_secret | ||
|
|
||
|
|
||
| # Hint: Null result if input is Null | ||
| @overload | ||
| def resolve_list_of_strings(value: None) -> None: ... | ||
|
|
||
|
|
||
| # Hint: Non-null result if input is non-null | ||
| @overload | ||
| def resolve_list_of_strings(value: str | list[str] | set[str]) -> list[str]: ... | ||
aaronsteers marked this conversation as resolved.
Dismissed
Show dismissed
Hide dismissed
|
||
|
|
||
|
|
||
| def resolve_list_of_strings(value: str | list[str] | set[str] | None) -> list[str] | None: | ||
| """Resolve a string or list of strings to a list of strings. | ||
|
|
||
| This method will handle three types of input: | ||
|
|
||
| 1. A list of strings (e.g., ["stream1", "stream2"]) will be returned as-is. | ||
| 2. None or empty input will return None. | ||
| 3. A single CSV string (e.g., "stream1,stream2") will be split into a list. | ||
| 4. A JSON string (e.g., '["stream1", "stream2"]') will be parsed into a list. | ||
| 5. If the input is empty or None, an empty list will be returned. | ||
|
|
||
| Args: | ||
| value: A string or list of strings. | ||
| """ | ||
| if value is None: | ||
| return None | ||
|
|
||
| if isinstance(value, list): | ||
| return value | ||
|
|
||
| if isinstance(value, set): | ||
| return list(value) | ||
|
|
||
| if not isinstance(value, str): | ||
| raise TypeError( | ||
| "Expected a string, list of strings, a set of strings, or None. " | ||
| f"Got '{type(value).__name__}': {value}" | ||
| ) | ||
|
|
||
| value = value.strip() | ||
| if not value: | ||
| return [] | ||
|
|
||
| if value.startswith("[") and value.endswith("]"): | ||
| # Try to parse as JSON array: | ||
| try: | ||
| parsed = json.loads(value) | ||
| if isinstance(parsed, list) and all(isinstance(item, str) for item in parsed): | ||
| return parsed | ||
| except json.JSONDecodeError as ex: | ||
| raise ValueError(f"Invalid JSON array: {value}") from ex | ||
|
|
||
| # Fallback to CSV split: | ||
| return [item.strip() for item in value.split(",") if item.strip()] | ||
aaronsteers marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
|
|
||
| def resolve_connector_config( # noqa: PLR0912 | ||
| config: dict | str | None = None, | ||
| config_file: str | Path | None = None, | ||
| config_secret_name: str | None = None, | ||
| config_spec_jsonschema: dict[str, Any] | None = None, | ||
| ) -> dict[str, Any]: | ||
| """Resolve a configuration dictionary, JSON string, or file path to a dictionary. | ||
|
|
||
| Returns: | ||
| Resolved configuration dictionary (empty if no inputs provided) | ||
|
|
||
| Raises: | ||
| ValueError: If JSON parsing fails or a provided input is invalid | ||
|
|
||
| We reject hardcoded secrets in a config dict if we detect them. | ||
| """ | ||
| config_dict: dict[str, Any] = {} | ||
|
|
||
| if config is None and config_file is None and config_secret_name is None: | ||
| return {} | ||
|
|
||
| if config_file is not None: | ||
| if isinstance(config_file, str): | ||
| config_file = Path(config_file) | ||
|
|
||
| if not isinstance(config_file, Path): | ||
| raise ValueError( | ||
| f"config_file must be a string or Path object, got: {type(config_file).__name__}" | ||
| ) | ||
|
|
||
| if not config_file.exists(): | ||
| raise FileNotFoundError(f"Configuration file not found: {config_file}") | ||
|
|
||
| def _raise_invalid_type(file_config: object) -> None: | ||
| raise TypeError( | ||
| f"Configuration file must contain a valid JSON/YAML object, " | ||
| f"got: {type(file_config).__name__}" | ||
| ) | ||
|
|
||
| try: | ||
| file_config = yaml.safe_load(config_file.read_text()) | ||
| if not isinstance(file_config, dict): | ||
| _raise_invalid_type(file_config) | ||
| config_dict.update(file_config) | ||
| except Exception as e: | ||
| raise ValueError(f"Error reading configuration file {config_file}: {e}") from e | ||
|
|
||
| if config is not None: | ||
| if isinstance(config, dict): | ||
| config_dict.update(config) | ||
| elif isinstance(config, str): | ||
| try: | ||
| parsed_config = json.loads(config) | ||
| if not isinstance(parsed_config, dict): | ||
| raise TypeError( | ||
| f"Parsed JSON config must be an object/dict, " | ||
| f"got: {type(parsed_config).__name__}" | ||
| ) | ||
| config_dict.update(parsed_config) | ||
| except json.JSONDecodeError as e: | ||
| raise ValueError(f"Invalid JSON in config parameter: {e}") from e | ||
| else: | ||
| raise ValueError(f"Config must be a dict or JSON string, got: {type(config).__name__}") | ||
|
|
||
| if config_dict and config_spec_jsonschema is not None: | ||
| hardcoded_secrets: list[list[str]] = detect_hardcoded_secrets( | ||
| config=config_dict, | ||
| spec_json_schema=config_spec_jsonschema, | ||
| ) | ||
| if hardcoded_secrets: | ||
| error_msg = "Configuration contains hardcoded secrets in fields: " | ||
| error_msg += ", ".join( | ||
| [".".join(hardcoded_secret) for hardcoded_secret in hardcoded_secrets] | ||
| ) | ||
|
|
||
| error_msg += ( | ||
| "Please use environment variables instead. For example:\n" | ||
| "To set a secret via reference, set its value to " | ||
| "`secret_reference::ENV_VAR_NAME`.\n" | ||
| ) | ||
| raise ValueError(error_msg) | ||
|
|
||
| if config_secret_name is not None: | ||
| # Assume this is a secret name that points to a JSON/YAML config. | ||
| secret_config = yaml.safe_load(str(get_secret(config_secret_name))) | ||
| if not isinstance(secret_config, dict): | ||
| raise ValueError( | ||
| f"Secret '{config_secret_name}' must contain a valid JSON or YAML object, " | ||
| f"but got: {type(secret_config).__name__}" | ||
| ) | ||
|
|
||
| # Merge the secret config into the main config: | ||
| deep_update( | ||
| config_dict, | ||
| secret_config, | ||
| ) | ||
|
|
||
| return config_dict | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.