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
30 changes: 30 additions & 0 deletions .cursor/commands/file-issue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# File Issue in GitHub

## Overview

The goal is to draft GitHub ticket as a markdown that can be copy pasted.

If avaialble use Github MCP.

## Workflow

When filing issues to i-am-bee/agentstack:

1. Read appropriate template from `.github/ISSUE_TEMPLATE/` first:
- `bug_report.md` for bugs
- `feature_request.md` for features

2. Follow template structure exactly

3. Ask the user to specify if something is unclear based on the template and their initial prompt.

4. If available use Github MCP to search duplicates

5. Before filing the ticket in github show the full ticket that you are about to file to have it confirmed by the user. Show this as a markdown.

6. Once user is happy, you can either submit the ticket if GH MCP is available otherwise dump as markdown.

## Rules

- Keep the ticket extremely concise
- Don't provide implementation details. Your goal is to define the problem and its potential consequences, not to propose a solution.
11 changes: 11 additions & 0 deletions .cursor/commands/find-breaking-changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Finding Breaking Changes

Repo is i-am-bee/agentstack

Finding breaking changes requires GitHub MCP. You need to find all merged PRs that has been merged in main since the last release tag.

If the PR introduces breaking change then it will be labeled with `breaking-change` label which you can use for filtering.

Knowing which PRs introdcude breaking change would allow you to go through all the comments and find the reason behind the breaking change.

List and provide all the PRs with brief descirptions about what are the introduced breaking changes.
16 changes: 16 additions & 0 deletions .cursor/commands/write-documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Write Documentation for Agent Stack

There's a common pattern in how documentation is written specifically in docs/agent-development folder. The folder serves to demonstrate all capabilities for agent builders.

There's a certain pattern how the documentation is written.

## How to write good documentation

1. Closely examine agent-development folder to see all patterns
2. Analyse user's prompt and think about proper plan how to ensure consistency
3. Analyse code base to see all the connections for what is being documented
4. Propose the documentation page


### Documentation Rules
- Don't use em dashes
11 changes: 10 additions & 1 deletion agentstack.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"name": "agentstack-sdk-py",
"path": "apps/agentstack-sdk-py"
},
{
"name": "agentstack-sdk-ts",
"path": "apps/agentstack-sdk-ts"
},
{
"name": "agentstack-cli",
"path": "apps/agentstack-cli"
Expand Down Expand Up @@ -47,7 +51,12 @@
"**/helm/templates/**/*.yaml": "helm",
"**/helm/templates/**/*.tpl": "helm"
},
"basedpyright.analysis.diagnosticMode": "openFilesOnly"
"basedpyright.analysis.diagnosticMode": "openFilesOnly",
"cursorpyright.analysis.diagnosticMode": "openFilesOnly",
"cursorpyright.analysis.extraPaths": [
"${workspaceFolder}/apps/agentstack-sdk-py/src"
],
"cursorpyright.analysis.typeCheckingMode": "basic"
},
"extensions": {
"recommendations": [
Expand Down
4 changes: 3 additions & 1 deletion apps/agentstack-cli/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"python.analysis.typeCheckingMode": "basic",
"python.analysis.diagnosticMode": "openFilesOnly"
"python.analysis.diagnosticMode": "openFilesOnly",
"cursorpyright.analysis.diagnosticMode": "openFilesOnly",
"cursorpyright.analysis.typeCheckingMode": "basic"
}
4 changes: 3 additions & 1 deletion apps/agentstack-sdk-py/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
"python.testing.pytestEnabled": true,
"cursorpyright.analysis.diagnosticMode": "openFilesOnly",
"cursorpyright.analysis.typeCheckingMode": "basic"
}
3 changes: 1 addition & 2 deletions apps/agentstack-sdk-py/examples/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import asyncclick.exceptions
import httpx
import yaml
from pydantic import AnyHttpUrl

import agentstack_sdk.a2a.extensions.services.llm

Expand Down Expand Up @@ -82,7 +81,7 @@ async def cli(base_url: str, context_id: str) -> None:
# Demonstration only: we ignore the asks and just configure Agent Stack proxy for everything
key: agentstack_sdk.a2a.extensions.services.mcp.MCPFulfillment(
transport=agentstack_sdk.a2a.extensions.services.mcp.StreamableHTTPTransport(
url=AnyHttpUrl("http://localhost:8333/mcp"),
url="http://localhost:8333/mcp",
),
)
for key in mcp_spec.params.mcp_demands
Expand Down
16 changes: 11 additions & 5 deletions apps/agentstack-sdk-py/examples/connectors_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,19 @@ async def connectors_agent(
yield "MCP extension hasn't been activated, no tools are available"
return

async with mcp.create_client() as (read, write), ClientSession(read, write) as session:
await session.initialize()
async with mcp.create_client() as client:
if client is None:
yield "MCP client not available."
return

tools = await session.list_tools()
read, write = client
async with ClientSession(read_stream=read, write_stream=write) as session:
await session.initialize()

yield "Available tools: \n"
yield "\n".join([t.name for t in tools.tools])
tools = await session.list_tools()

yield "Available tools: \n"
yield "\n".join([t.name for t in tools.tools])


if __name__ == "__main__":
Expand Down
4 changes: 2 additions & 2 deletions apps/agentstack-sdk-py/examples/connectors_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import a2a.client
import a2a.types
import httpx
from pydantic import AnyHttpUrl, HttpUrl
from pydantic import HttpUrl

import agentstack_sdk.a2a.extensions
from agentstack_sdk.a2a.extensions.services.platform import PlatformApiExtensionClient
Expand Down Expand Up @@ -53,7 +53,7 @@ async def run(
mcp_fulfillments={
key: agentstack_sdk.a2a.extensions.services.mcp.MCPFulfillment(
transport=agentstack_sdk.a2a.extensions.services.mcp.StreamableHTTPTransport(
url=AnyHttpUrl("http://{platform_url}" + f"/api/v1/connectors/{connector_id}/mcp")
url="{platform_url}" + f"/api/v1/connectors/{connector_id}/mcp"
),
)
for key in mcp_spec.params.mcp_demands
Expand Down
45 changes: 45 additions & 0 deletions apps/agentstack-sdk-py/examples/github_mcp_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright 2025 © BeeAI a Series of LF Projects, LLC
# SPDX-License-Identifier: Apache-2.0

import json
from collections.abc import AsyncGenerator
from typing import Annotated

from a2a.types import Message
from mcp import ClientSession

from agentstack_sdk.a2a.extensions.services.mcp import MCPServiceExtensionServer, MCPServiceExtensionSpec
from agentstack_sdk.a2a.types import RunYield
from agentstack_sdk.server import Server

server = Server()


@server.agent()
async def github_mcp_agent(
mcp_service: Annotated[
MCPServiceExtensionServer,
MCPServiceExtensionSpec.single_demand(suggested=("github",)),
],
) -> AsyncGenerator[RunYield, Message]:
"""Lists tools"""

if not mcp_service:
yield "MCP extension hasn't been activated, no tools are available"
return

async with mcp_service.create_client() as client:
if client is None:
yield "MCP client not available."
return

read, write = client
async with ClientSession(read_stream=read, write_stream=write) as session:
await session.initialize()
me_result = await session.call_tool("get_me", {})
result_dict = me_result.model_dump() if hasattr(me_result, "model_dump") else me_result
yield json.dumps(result_dict, indent=2, default=str)


if __name__ == "__main__":
server.run()
44 changes: 25 additions & 19 deletions apps/agentstack-sdk-py/examples/mcp_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,28 +42,34 @@ async def mcp_agent(
if not mcp_tool_call:
yield "MCP Tool Call extension hasn't been activated, no approval requests will be issued"

async with (
mcp_service.create_client() as (read, write),
ClientSession(read, write) as session,
):
session_init_result = await session.initialize()
async with mcp_service.create_client() as client:
if client is None:
yield "MCP client not available."
return

result = await session.list_tools()
read, write = client
async with (
ClientSession(read_stream=read, write_stream=write) as session,
):
session_init_result = await session.initialize()

yield "Available tools: \n"
yield "\n".join([t.name for t in result.tools])
result = await session.list_tools()

if result.tools:
tool = result.tools[0]
input = {}
yield f"Requesting approval for tool {tool.name}"
if mcp_tool_call:
await mcp_tool_call.request_tool_call_approval(
ToolCallRequest.from_mcp_tool(tool, input, server=session_init_result.serverInfo), context=context
)
yield f"Calling tool {tool.name}"
await session.call_tool(tool.name, input)
yield "Tool call finished"
yield "Available tools: \n"
yield "\n".join([t.name for t in result.tools])

if result.tools:
tool = result.tools[0]
input = {}
yield f"Requesting approval for tool {tool.name}"
if mcp_tool_call:
await mcp_tool_call.request_tool_call_approval(
ToolCallRequest.from_mcp_tool(tool, input, server=session_init_result.serverInfo),
context=context,
)
yield f"Calling tool {tool.name}"
await session.call_tool(tool.name, input)
yield "Tool call finished"


if __name__ == "__main__":
Expand Down
4 changes: 2 additions & 2 deletions apps/agentstack-sdk-py/examples/mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import a2a.types
import httpx
from aiohttp import web
from pydantic import AnyHttpUrl, AnyUrl
from pydantic import AnyUrl

import agentstack_sdk.a2a.extensions
from agentstack_sdk.a2a.extensions.tools.call import ToolCallResponse
Expand Down Expand Up @@ -92,7 +92,7 @@ async def run(base_url: str = "http://127.0.0.1:10000"):
mcp_fulfillments={
key: agentstack_sdk.a2a.extensions.services.mcp.MCPFulfillment(
transport=agentstack_sdk.a2a.extensions.services.mcp.StreamableHTTPTransport(
url=AnyHttpUrl("https://mcp.stripe.com")
url="https://mcp.stripe.com"
),
)
for key in mcp_service_spec.params.mcp_demands
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class StdioTransport(pydantic.BaseModel):
class StreamableHTTPTransport(pydantic.BaseModel):
type: Literal["streamable_http"] = "streamable_http"

url: pydantic.AnyHttpUrl
url: str
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that client can provide {platform_url}/foobar like in other url fields.

headers: dict[str, str] | None = None


Expand Down Expand Up @@ -111,13 +111,7 @@ def handle_incoming_message(self, message: a2a.types.Message, context: RunContex
for fullfilment in self.data.mcp_fulfillments.values():
if fullfilment.transport.type == "streamable_http":
try:
fullfilment.transport.url = pydantic.AnyHttpUrl(
re.sub(
r"^http[s]?://{platform_url}",
platform_url,
str(fullfilment.transport.url),
)
)
fullfilment.transport.url = re.sub("^{platform_url}", platform_url, str(fullfilment.transport.url))
except Exception:
logger.warning("Platform URL substitution failed", exc_info=True)

Expand All @@ -126,7 +120,7 @@ def parse_client_metadata(self, message: a2a.types.Message) -> MCPServiceExtensi
if metadata:
for name, demand in self.spec.params.mcp_demands.items():
if not (fulfillment := metadata.mcp_fulfillments.get(name)):
raise ValueError(f'Fulfillment for demand "{name}" missing')
continue
if fulfillment.transport.type not in demand.allowed_transports:
raise ValueError(f'Transport "{fulfillment.transport.type}" not allowed for demand "{name}"')
return metadata
Expand All @@ -148,7 +142,8 @@ async def create_client(self, demand: str = _DEFAULT_DEMAND_NAME):
fulfillment = self.data.mcp_fulfillments.get(demand) if self.data else None

if not fulfillment:
raise ValueError(f'No fulfillment for demand "{demand}"')
yield None
return

transport = fulfillment.transport

Expand All @@ -162,7 +157,7 @@ async def create_client(self, demand: str = _DEFAULT_DEMAND_NAME):
yield (read, write)
elif isinstance(transport, StreamableHTTPTransport):
async with streamablehttp_client(
url=str(transport.url),
url=transport.url,
headers=transport.headers,
auth=await self._create_auth(transport),
) as (
Expand All @@ -180,12 +175,12 @@ async def _create_auth(self, transport: StreamableHTTPTransport):
platform
and platform.data
and platform.data.base_url
and str(transport.url).startswith(str(platform.data.base_url))
and transport.url.startswith(str(platform.data.base_url))
):
return await platform.create_httpx_auth()
oauth = self._get_oauth_server()
if oauth:
return await oauth.create_httpx_auth(resource_url=transport.url)
return await oauth.create_httpx_auth(resource_url=pydantic.AnyUrl(transport.url))
return None


Expand Down
Loading