Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions examples/mcp/mcp_elicitation/cloud/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

app = MCPApp(
name="elicitation_demo",
description="Demo of workflow with elicitation"
)
app = MCPApp(name="elicitation_demo", description="Demo of workflow with elicitation")


# mcp_context for fastmcp context
Expand All @@ -24,7 +21,9 @@ class ConfirmBooking(BaseModel):
confirm: bool = Field(description="Confirm booking?")
notes: str = Field(default="", description="Special requests")

app.logger.info(f"Confirming the use wants to book a table for {party_size} on {date} via elicitation")
app.logger.info(
f"Confirming the use wants to book a table for {party_size} on {date} via elicitation"
)

result = await app.context.upstream_session.elicit(
message=f"Confirm booking for {party_size} on {date}?",
Expand All @@ -42,4 +41,3 @@ class ConfirmBooking(BaseModel):
return "Booking declined"
elif result.action == "cancel":
return "Booking cancelled"

52 changes: 43 additions & 9 deletions src/mcp_agent/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,17 @@
import functools

from types import MethodType
from typing import Any, Dict, Optional, Type, TypeVar, Callable, TYPE_CHECKING
from typing import (
Any,
Dict,
Optional,
Type,
TypeVar,
Callable,
TYPE_CHECKING,
ParamSpec,
overload,
)
from datetime import timedelta
from contextlib import asynccontextmanager

Expand Down Expand Up @@ -36,6 +46,7 @@
from mcp_agent.agents.agent_spec import AgentSpec
from mcp_agent.executor.workflow import Workflow

P = ParamSpec("P")
R = TypeVar("R")


Expand Down Expand Up @@ -714,13 +725,25 @@ async def _run(self, *args, **kwargs): # type: ignore[no-redef]
self.workflow(auto_cls, workflow_id=workflow_name)
return auto_cls

@overload
def tool(self, __fn: Callable[P, R]) -> Callable[P, R]: ...

@overload
def tool(
self,
name: str | None = None,
*,
description: str | None = None,
structured_output: bool | None = None,
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...

def tool(
self,
name: str | None = None,
*,
description: str | None = None,
structured_output: bool | None = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
):
"""
Decorator to declare a synchronous MCP tool that runs via an auto-generated
Workflow and waits for completion before returning.
Expand All @@ -729,7 +752,7 @@ def tool(
endpoints are available.
"""

def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
def decorator(fn: Callable[P, R]) -> Callable[P, R]:
tool_name = name or fn.__name__

# Early validation: Use the shared tool adapter logic to validate
Expand Down Expand Up @@ -762,26 +785,37 @@ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:

# Support bare usage: @app.tool without parentheses
if callable(name) and description is None and structured_output is None:
fn = name # type: ignore[assignment]
_fn = name # type: ignore[assignment]
name = None
return decorator(fn) # type: ignore[arg-type]
return decorator(_fn) # type: ignore[arg-type]

return decorator

@overload
def async_tool(self, __fn: Callable[P, R]) -> Callable[P, R]: ...

@overload
def async_tool(
self,
name: str | None = None,
*,
description: str | None = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...

def async_tool(
self,
name: str | None = None,
*,
description: str | None = None,
):
"""
Decorator to declare an asynchronous MCP tool.

Creates a Workflow class from the function and registers it so that
the standard per-workflow tools (run/get_status) are exposed by the server.
"""

def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:
def decorator(fn: Callable[P, R]) -> Callable[P, R]:
workflow_name = name or fn.__name__

# Early validation: Use the shared tool adapter logic to validate
Expand Down Expand Up @@ -812,9 +846,9 @@ def decorator(fn: Callable[..., Any]) -> Callable[..., Any]:

# Support bare usage: @app.async_tool without parentheses
if callable(name) and description is None:
fn = name # type: ignore[assignment]
_fn = name # type: ignore[assignment]
name = None
return decorator(fn) # type: ignore[arg-type]
return decorator(_fn) # type: ignore[arg-type]

return decorator

Expand Down
75 changes: 61 additions & 14 deletions src/mcp_agent/cli/commands/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,35 @@ def _write(path: Path, content: str, force: bool) -> bool:
return False


def _write_readme(dir_path: Path, content: str, force: bool) -> str | None:
"""Create a README file with fallback naming if a README already exists.

Returns the filename created, or None if it could not be written (in which case
the content is printed to console as a fallback).
"""
candidates = [
"README.md",
"README.mcp-agent.md",
"README.mcp.md",
]
# Add numeric fallbacks
candidates += [f"README.{i}.md" for i in range(1, 6)]

for name in candidates:
path = dir_path / name
if not path.exists() or force:
ok = _write(path, content, force)
if ok:
return name
# Fallback: print content to console if we couldn't write any variant
console.print(
"\n[yellow]A README already exists and could not be overwritten.[/yellow]"
)
console.print("[bold]Suggested README contents:[/bold]\n")
console.print(content)
return None


@app.callback(invoke_without_command=True)
def init(
ctx: typer.Context,
Expand Down Expand Up @@ -136,7 +165,7 @@ def init(
else:
# Ask for an alternate filename and ensure it ends with .py
alt_name = Prompt.ask(
"Enter a filename to save the agent", default="agent.py"
"Enter a filename to save the agent", default="main.py"
)
if not alt_name.endswith(".py"):
alt_name += ".py"
Expand All @@ -153,6 +182,15 @@ def init(
except Exception:
pass

# No separate agents.yaml needed; agent definitions live in mcp_agent.config.yaml

# Create README for the basic template
readme_content = _load_template("README_init.md")
if readme_content:
created = _write_readme(dir, readme_content, force)
if created:
files_created.append(created)

elif template == "server":
server_path = dir / "server.py"
server_content = _load_template("basic_agent_server.py")
Expand All @@ -164,6 +202,13 @@ def init(
except Exception:
pass

# README for server template
readme_content = _load_template("README_init.md")
if readme_content:
created = _write_readme(dir, readme_content, force)
if created:
files_created.append(created)

elif template == "token":
token_path = dir / "token_example.py"
token_content = _load_template("token_counter.py")
Expand All @@ -175,6 +220,12 @@ def init(
except Exception:
pass

readme_content = _load_template("README_init.md")
if readme_content:
created = _write_readme(dir, readme_content, force)
if created:
files_created.append(created)

elif template == "factory":
factory_path = dir / "factory.py"
factory_content = _load_template("agent_factory.py")
Expand All @@ -192,6 +243,12 @@ def init(
if agents_content and _write(agents_path, agents_content, force):
files_created.append("agents.yaml")

readme_content = _load_template("README_init.md")
if readme_content:
created = _write_readme(dir, readme_content, force)
if created:
files_created.append(created)

# Display results
if files_created:
console.print("\n[green]✅ Successfully initialized project![/green]")
Expand All @@ -208,16 +265,6 @@ def init(
if template == "basic":
run_file = entry_script_name or "main.py"
console.print(f"3. Run your agent: [cyan]uv run {run_file}[/cyan]")
console.print(
f" Or use: [cyan]mcp-agent dev start --script {run_file}[/cyan]"
)
console.print(
f" Or serve: [cyan]mcp-agent dev serve --script {run_file}[/cyan]"
)
console.print(" Or chat: [cyan]mcp-agent dev chat[/cyan]")
console.print(
"4. Edit config: [cyan]mcp-agent config edit[/cyan] (then rerun)"
)
elif template == "server":
console.print("3. Run the server: [cyan]uv run server.py[/cyan]")
console.print(
Expand All @@ -229,9 +276,9 @@ def init(
elif template == "factory":
console.print("3. Customize agents in [cyan]agents.yaml[/cyan]")
console.print("4. Run the factory: [cyan]uv run factory.py[/cyan]")
elif template == "minimal":
console.print("3. Create your agent script")
console.print(" See examples: [cyan]mcp-agent quickstart[/cyan]")
elif template == "minimal":
console.print("3. Create your agent script")
console.print(" See examples: [cyan]mcp-agent quickstart[/cyan]")

console.print(
"\n[dim]Run [cyan]mcp-agent doctor[/cyan] to check your configuration[/dim]"
Expand Down
47 changes: 47 additions & 0 deletions src/mcp_agent/cli/commands/quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ def overview() -> None:
("token-counter", "data/examples/basic/token_counter"),
("agent-factory", "data/examples/basic/agent_factory"),
("basic-agent-server", "data/examples/mcp_agent_server/asyncio"),
("reference-agent-server", "data/examples/mcp_agent_server/reference"),
("elicitation", "data/examples/mcp_agent_server/elicitation"),
("sampling", "data/examples/mcp_agent_server/sampling"),
("notifications", "data/examples/mcp_agent_server/notifications"),
]
for n, p in rows:
table.add_row(n, p)
Expand Down Expand Up @@ -199,3 +203,46 @@ def basic_agent_server(
src = EXAMPLE_ROOT / "mcp_agent_server" / "asyncio"
copied = _copy_tree(src, dst, force)
console.print(f"Copied {copied} set(s) to {dst}")


@app.command("reference-agent-server")
def reference_agent_server(
dir: Path = typer.Argument(Path(".")),
force: bool = typer.Option(False, "--force", "-f"),
) -> None:
dst = dir.resolve() / "reference_agent_server"
copied = _copy_pkg_tree("mcp_agent_server/reference", dst, force)
if not copied:
src = EXAMPLE_ROOT / "mcp_agent_server" / "reference"
copied = _copy_tree(src, dst, force)
console.print(f"Copied {copied} set(s) to {dst}")


@app.command("elicitation")
def elicitation(
dir: Path = typer.Argument(Path(".")),
force: bool = typer.Option(False, "--force", "-f"),
) -> None:
dst = dir.resolve() / "elicitation"
copied = _copy_pkg_tree("mcp_agent_server/elicitation", dst, force)
console.print(f"Copied {copied} set(s) to {dst}")


@app.command("sampling")
def sampling(
dir: Path = typer.Argument(Path(".")),
force: bool = typer.Option(False, "--force", "-f"),
) -> None:
dst = dir.resolve() / "sampling"
copied = _copy_pkg_tree("mcp_agent_server/sampling", dst, force)
console.print(f"Copied {copied} set(s) to {dst}")


@app.command("notifications")
def notifications(
dir: Path = typer.Argument(Path(".")),
force: bool = typer.Option(False, "--force", "-f"),
) -> None:
dst = dir.resolve() / "notifications"
copied = _copy_pkg_tree("mcp_agent_server/notifications", dst, force)
console.print(f"Copied {copied} set(s) to {dst}")
33 changes: 33 additions & 0 deletions src/mcp_agent/data/examples/mcp_agent_server/elicitation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Elicitation Server

Minimal server demonstrating user confirmation via elicitation.

## Run

```bash
uv run server.py
```

Connect with the minimal client:

```bash
uv run client.py
```

Tools:

- `confirm_action(action: str)` — prompts the user (via upstream client) to accept or decline.

This example uses console handlers for local testing. In an MCP client UI, the prompt will be displayed to the user.

## Deploy to Cloud (optional)

1. Set your API keys in `mcp_agent.secrets.yaml`.

2. From this directory, deploy:

```bash
uv run mcp-agent deploy elicitation-example
```

You’ll receive an app ID and a URL. Use the URL with an MCP client (e.g., MCP Inspector) and append `/sse` to the end. Set the Bearer token in the header to your mcp-agent API key.
Loading
Loading