Skip to content

Commit 3ef976f

Browse files
committed
Merge remote-tracking branch 'upstream/main' into feat/anthropic-prompt-caching
2 parents 9faf69a + 00ad32e commit 3ef976f

27 files changed

+2566
-1402
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ examples/mcp_root_test/test_data/*.png
168168
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
169169
# and can be added to the global gitignore or merged into this file. For a more nuclear
170170
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
171-
#.idea/
171+
.idea/
172172
uv.lock
173173

174174
# File generated from promptify script (to create an LLM-friendly prompt for the repo)

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@ Start by installing the [uv package manager](https://docs.astral.sh/uv/) for Pyt
3838

3939
```bash
4040
uv pip install fast-agent-mcp # install fast-agent!
41-
42-
uv run fast-agent setup # create an example agent and config files
41+
fast-agent go # start an interactive session
42+
fast-agent go https://hf.co/mcp # with a remote MCP
43+
fast-agent go --model=generic.qwen2.5 # use ollama qwen 2.5
44+
fast-agent setup # create an example agent and config files
4345
uv run agent.py # run your first agent
4446
uv run agent.py --model=o3-mini.low # specify a model
45-
uv run fast-agent quickstart workflow # create "building effective agents" examples
47+
fast-agent quickstart workflow # create "building effective agents" examples
4648
```
4749

4850
Other quickstart examples include a Researcher Agent (with Evaluator-Optimizer workflow) and Data Analysis Agent (similar to the ChatGPT experience), demonstrating MCP Roots support.

pyproject.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "fast-agent-mcp"
3-
version = "0.2.28"
3+
version = "0.2.30"
44
description = "Define, Prompt and Test MCP enabled Agents and Workflows"
55
readme = "README.md"
66
license = { file = "LICENSE" }
@@ -15,7 +15,7 @@ classifiers = [
1515
requires-python = ">=3.10"
1616
dependencies = [
1717
"fastapi>=0.115.6",
18-
"mcp==1.9.1",
18+
"mcp==1.9.3",
1919
"opentelemetry-distro>=0.50b0",
2020
"opentelemetry-exporter-otlp-proto-http>=1.29.0",
2121
"pydantic-settings>=2.7.0",
@@ -29,14 +29,14 @@ dependencies = [
2929
"prompt-toolkit>=3.0.50",
3030
"aiohttp>=3.11.13",
3131
"a2a-types>=0.1.0",
32-
"opentelemetry-instrumentation-openai>=0.39.3; python_version >= '3.10' and python_version < '4.0'",
33-
"opentelemetry-instrumentation-anthropic>=0.39.3; python_version >= '3.10' and python_version < '4.0'",
34-
"opentelemetry-instrumentation-mcp>=0.40.3; python_version >= '3.10' and python_version < '4.0'",
32+
"opentelemetry-instrumentation-openai>=0.0.40.7; python_version >= '3.10' and python_version < '4.0'",
33+
"opentelemetry-instrumentation-anthropic>=0.40.7; python_version >= '3.10' and python_version < '4.0'",
34+
"opentelemetry-instrumentation-mcp>=0.40.7; python_version >= '3.10' and python_version < '4.0'",
3535
"google-genai",
3636
"opentelemetry-instrumentation-google-genai>=0.2b0",
3737
"tensorzero>=2025.4.7",
38-
"google-genai",
3938
"opentelemetry-instrumentation-google-genai>=0.2b0",
39+
"deprecated>=1.2.18",
4040
]
4141

4242
[project.optional-dependencies]

src/mcp_agent/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ async def initialize(self) -> None:
119119
if self._initialized:
120120
return
121121

122-
self._context = await initialize_context(self._config_or_path)
122+
self._context = await initialize_context(self._config_or_path, store_globally=True)
123123

124124
# Set the properties that were passed in the constructor
125125
self._context.human_input_handler = self._human_input_callback

src/mcp_agent/cli/commands/check_config.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,17 @@ def get_config_summary(config_path: Optional[Path]) -> dict:
226226

227227
# Determine transport type
228228
if "url" in server_config:
229-
server_info["transport"] = "SSE"
230-
server_info["url"] = server_config.get("url", "")
229+
url = server_config.get("url", "")
230+
server_info["url"] = url
231+
232+
# Use URL path to determine transport type
233+
try:
234+
from .url_parser import parse_server_url
235+
_, transport_type, _ = parse_server_url(url)
236+
server_info["transport"] = transport_type.upper()
237+
except Exception:
238+
# Fallback to HTTP if URL parsing fails
239+
server_info["transport"] = "HTTP"
231240

232241
# Get command and args
233242
command = server_config.get("command", "")

src/mcp_agent/cli/commands/url_parser.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from typing import Dict, List, Literal, Tuple
99
from urllib.parse import urlparse
1010

11+
from mcp_agent.mcp.hf_auth import add_hf_auth_header
12+
1113

1214
def parse_server_url(
1315
url: str,
@@ -131,7 +133,11 @@ def parse_server_urls(
131133
result = []
132134
for url in url_list:
133135
server_name, transport_type, parsed_url = parse_server_url(url)
134-
result.append((server_name, transport_type, parsed_url, headers))
136+
137+
# Apply HuggingFace authentication if appropriate
138+
final_headers = add_hf_auth_header(parsed_url, headers)
139+
140+
result.append((server_name, transport_type, parsed_url, final_headers))
135141

136142
return result
137143

src/mcp_agent/config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,15 @@ class TensorZeroSettings(BaseModel):
230230
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
231231

232232

233+
class HuggingFaceSettings(BaseModel):
234+
"""
235+
Settings for HuggingFace authentication (used for MCP connections).
236+
"""
237+
238+
api_key: Optional[str] = None
239+
model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
240+
241+
233242
class LoggerSettings(BaseModel):
234243
"""
235244
Logger settings for the fast-agent application.
@@ -330,6 +339,12 @@ class Settings(BaseSettings):
330339
azure: AzureSettings | None = None
331340
"""Settings for using Azure OpenAI Service in the fast-agent application"""
332341

342+
aliyun: OpenAISettings | None = None
343+
"""Settings for using Aliyun OpenAI Service in the fast-agent application"""
344+
345+
huggingface: HuggingFaceSettings | None = None
346+
"""Settings for HuggingFace authentication (used for MCP connections)"""
347+
333348
logger: LoggerSettings | None = LoggerSettings()
334349
"""Logger settings for the fast-agent application"""
335350

src/mcp_agent/context.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
1313
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
1414
from opentelemetry.instrumentation.google_genai import GoogleGenAiSdkInstrumentor
15-
from opentelemetry.instrumentation.mcp import McpInstrumentor
15+
16+
# from opentelemetry.instrumentation.mcp import McpInstrumentor
1617
from opentelemetry.instrumentation.openai import OpenAIInstrumentor
1718
from opentelemetry.propagate import set_global_textmap
1819
from opentelemetry.sdk.resources import Resource
@@ -114,7 +115,9 @@ async def configure_otel(config: "Settings") -> None:
114115
AnthropicInstrumentor().instrument()
115116
OpenAIInstrumentor().instrument()
116117
GoogleGenAiSdkInstrumentor().instrument()
117-
McpInstrumentor().instrument()
118+
119+
120+
# McpInstrumentor().instrument()
118121

119122

120123
async def configure_logger(config: "Settings") -> None:
@@ -198,7 +201,6 @@ async def cleanup_context() -> None:
198201
def get_current_context() -> Context:
199202
"""
200203
Synchronous initializer/getter for global application context.
201-
For async usage, use aget_current_context instead.
202204
"""
203205
global _global_context
204206
if _global_context is None:

src/mcp_agent/core/enhanced_prompt.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
Enhanced prompt functionality with advanced prompt_toolkit features.
33
"""
44

5+
import asyncio
6+
import os
7+
import shlex
8+
import subprocess
9+
import tempfile
510
from importlib.metadata import version
611
from typing import List, Optional
712

@@ -96,6 +101,85 @@ def get_completions(self, document, complete_event):
96101
)
97102

98103

104+
# Helper function to open text in an external editor
105+
def get_text_from_editor(initial_text: str = "") -> str:
106+
"""
107+
Opens the user\'s configured editor ($VISUAL or $EDITOR) to edit the initial_text.
108+
Falls back to \'nano\' (Unix) or \'notepad\' (Windows) if neither is set.
109+
Returns the edited text, or the original text if an error occurs.
110+
"""
111+
editor_cmd_str = os.environ.get("VISUAL") or os.environ.get("EDITOR")
112+
113+
if not editor_cmd_str:
114+
if os.name == "nt": # Windows
115+
editor_cmd_str = "notepad"
116+
else: # Unix-like (Linux, macOS)
117+
editor_cmd_str = "nano" # A common, usually available, simple editor
118+
119+
# Use shlex.split to handle editors with arguments (e.g., "code --wait")
120+
try:
121+
editor_cmd_list = shlex.split(editor_cmd_str)
122+
if not editor_cmd_list: # Handle empty string from shlex.split
123+
raise ValueError("Editor command string is empty or invalid.")
124+
except ValueError as e:
125+
rich_print(f"[red]Error: Invalid editor command string ('{editor_cmd_str}'): {e}[/red]")
126+
return initial_text
127+
128+
# Create a temporary file for the editor to use.
129+
# Using a suffix can help some editors with syntax highlighting or mode.
130+
try:
131+
with tempfile.NamedTemporaryFile(
132+
mode="w+", delete=False, suffix=".txt", encoding="utf-8"
133+
) as tmp_file:
134+
if initial_text:
135+
tmp_file.write(initial_text)
136+
tmp_file.flush() # Ensure content is written to disk before editor opens it
137+
temp_file_path = tmp_file.name
138+
except Exception as e:
139+
rich_print(f"[red]Error: Could not create temporary file for editor: {e}[/red]")
140+
return initial_text
141+
142+
try:
143+
# Construct the full command: editor_parts + [temp_file_path]
144+
# e.g., [\'vim\', \'/tmp/somefile.txt\'] or [\'code\', \'--wait\', \'/tmp/somefile.txt\']
145+
full_cmd = editor_cmd_list + [temp_file_path]
146+
147+
# Run the editor. This is a blocking call.
148+
subprocess.run(full_cmd, check=True)
149+
150+
# Read the content back from the temporary file.
151+
with open(temp_file_path, "r", encoding="utf-8") as f:
152+
edited_text = f.read()
153+
154+
except FileNotFoundError:
155+
rich_print(
156+
f"[red]Error: Editor command '{editor_cmd_list[0]}' not found. "
157+
f"Please set $VISUAL or $EDITOR correctly, or install '{editor_cmd_list[0]}'.[/red]"
158+
)
159+
return initial_text
160+
except subprocess.CalledProcessError as e:
161+
rich_print(
162+
f"[red]Error: Editor '{editor_cmd_list[0]}' closed with an error (code {e.returncode}).[/red]"
163+
)
164+
return initial_text
165+
except Exception as e:
166+
rich_print(
167+
f"[red]An unexpected error occurred while launching or using the editor: {e}[/red]"
168+
)
169+
return initial_text
170+
finally:
171+
# Always attempt to clean up the temporary file.
172+
if "temp_file_path" in locals() and os.path.exists(temp_file_path):
173+
try:
174+
os.remove(temp_file_path)
175+
except Exception as e:
176+
rich_print(
177+
f"[yellow]Warning: Could not remove temporary file {temp_file_path}: {e}[/yellow]"
178+
)
179+
180+
return edited_text.strip() # Added strip() to remove trailing newlines often added by editors
181+
182+
99183
def create_keybindings(on_toggle_multiline=None, app=None):
100184
"""Create custom key bindings."""
101185
kb = KeyBindings()
@@ -140,6 +224,27 @@ def _(event) -> None:
140224
"""Ctrl+L: Clear the input buffer."""
141225
event.current_buffer.text = ""
142226

227+
@kb.add("c-e")
228+
async def _(event) -> None:
229+
"""Ctrl+E: Edit current buffer in $EDITOR."""
230+
current_text = event.app.current_buffer.text
231+
try:
232+
# Run the synchronous editor function in a thread
233+
edited_text = await event.app.loop.run_in_executor(
234+
None, get_text_from_editor, current_text
235+
)
236+
event.app.current_buffer.text = edited_text
237+
# Optionally, move cursor to the end of the edited text
238+
event.app.current_buffer.cursor_position = len(edited_text)
239+
except asyncio.CancelledError:
240+
rich_print("[yellow]Editor interaction cancelled.[/yellow]")
241+
except Exception as e:
242+
rich_print(f"[red]Error during editor interaction: {e}[/red]")
243+
finally:
244+
# Ensure the UI is updated
245+
if event.app:
246+
event.app.invalidate()
247+
143248
return kb
144249

145250

src/mcp_agent/core/interactive_prompt.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ async def _select_prompt(
351351
for prompt in prompts:
352352
# Get basic prompt info
353353
prompt_name = getattr(prompt, "name", "Unknown")
354-
description = getattr(prompt, "description", "No description")
354+
prompt_description = getattr(prompt, "description", "No description")
355355

356356
# Extract argument information
357357
arg_names = []
@@ -387,7 +387,7 @@ async def _select_prompt(
387387
"server": server_name,
388388
"name": prompt_name,
389389
"namespaced_name": namespaced_name,
390-
"description": description,
390+
"description": prompt_description,
391391
"arg_count": len(arg_names),
392392
"arg_names": arg_names,
393393
"required_args": required_args,

0 commit comments

Comments
 (0)