vs-agent 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vs_agent/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from vs_agent.server.vs_agent_server import VsAgentServer
2
+
3
+ __all__ = [
4
+ "VsAgentServer",
5
+ ]
File without changes
@@ -0,0 +1,16 @@
1
+ from typing import List, Optional
2
+
3
+ from vs_security.guard.vs_security import get_auth_context
4
+
5
+
6
+ class VsActionSecurity:
7
+
8
+ def __init__(self, roles: Optional[List[str]] = None):
9
+ self._roles = roles or []
10
+
11
+ async def __call__(self) -> None:
12
+ ctx = get_auth_context()
13
+ if ctx is None:
14
+ raise PermissionError("Unauthenticated request")
15
+ if self._roles and not ctx.has_any_role(*self._roles):
16
+ raise PermissionError(f"Required roles: {self._roles}")
File without changes
@@ -0,0 +1,72 @@
1
+ import uuid
2
+ from datetime import datetime, timedelta, timezone
3
+ from typing import Any, Dict, Optional
4
+
5
+ import httpx
6
+ import jwt
7
+
8
+ from vs_agent.discovery.vs_peer_agent_registry import VsPeerAgentRegistry
9
+ from vs_common.config.vs_base_config import VsBaseConfig
10
+ from vs_common.log.vs_log_manager import VsLogManager
11
+
12
+ _SERVICE_ID = str(uuid.uuid5(uuid.NAMESPACE_DNS, "vs-agent-peer-client"))
13
+ _logger = VsLogManager.get_instance("VsPeerAgentClient")
14
+
15
+
16
+ class VsPeerAgentClient:
17
+
18
+ _secret: Optional[str] = None
19
+ _algorithm: str = "HS256"
20
+ _expiry_minutes: int = 2
21
+
22
+ @classmethod
23
+ def init(cls, config: VsBaseConfig) -> None:
24
+ cls._secret = config.get("agent_client.secret_key", default=None)
25
+ cls._algorithm = config.get("agent_client.algorithm", default="HS256")
26
+ cls._expiry_minutes = config.get("agent_client.access_expiry_minutes", default=2, data_type=int)
27
+
28
+ @classmethod
29
+ def _mint_token(cls) -> Optional[str]:
30
+ if not cls._secret:
31
+ return None
32
+ now = datetime.now(timezone.utc)
33
+ payload = {
34
+ "sub": _SERVICE_ID,
35
+ "username": "vs-agent",
36
+ "roles": ["service"],
37
+ "provider": "internal",
38
+ "type": "access",
39
+ "iat": now,
40
+ "exp": now + timedelta(minutes=cls._expiry_minutes),
41
+ }
42
+ return jwt.encode(payload, cls._secret, algorithm=cls._algorithm)
43
+
44
+ @classmethod
45
+ def _headers(cls) -> Dict[str, str]:
46
+ token = cls._mint_token()
47
+ return {"Authorization": f"Bearer {token}"} if token else {}
48
+
49
+ @classmethod
50
+ async def call(
51
+ cls,
52
+ agent: str,
53
+ path: str,
54
+ payload: Dict[str, Any],
55
+ method: str = "POST",
56
+ timeout: float = 230.0,
57
+ ) -> dict:
58
+ entry = VsPeerAgentRegistry.get(agent)
59
+ if entry is None:
60
+ raise ValueError(f"Agent '{agent}' is not registered.")
61
+ if entry.status == "offline":
62
+ raise RuntimeError(f"Agent '{agent}' is currently offline.")
63
+
64
+ url = f"{entry.url}{path}"
65
+ _logger.info(f"Calling peer agent '{agent}' → {method} {url}")
66
+
67
+ async with httpx.AsyncClient(timeout=timeout) as client:
68
+ response = await getattr(client, method.lower())(
69
+ url, json=payload, headers=cls._headers()
70
+ )
71
+ response.raise_for_status()
72
+ return response.json()
File without changes
@@ -0,0 +1,23 @@
1
+ from typing import Any, Callable, Dict, List, Optional
2
+
3
+ from vs_agent.registry.vs_action_registry import VsActionRegistry
4
+ from vs_agent.schema.vs_action_schema import Intent
5
+ from vs_mcp_agent.registry.vs_tool_registry import VsToolRegistry
6
+
7
+
8
+ def action(
9
+ name: str,
10
+ description: str,
11
+ path: str,
12
+ method: str = "POST",
13
+ intents: Optional[List[Intent]] = None,
14
+ input_schema: Optional[Dict[str, Any]] = None,
15
+ guards: Optional[List[Callable]] = None,
16
+ ):
17
+ def decorator(fn: Callable) -> Callable:
18
+ _guards = guards or []
19
+ _intents = intents or []
20
+ VsToolRegistry.register_fn(name, description, fn, guards=_guards)
21
+ VsActionRegistry.register(name, description, path, method, _intents, input_schema, _guards, fn)
22
+ return fn
23
+ return decorator
File without changes
@@ -0,0 +1,73 @@
1
+ import threading
2
+ from dataclasses import dataclass, field
3
+ from datetime import datetime, timezone
4
+ from typing import Dict, List, Optional
5
+
6
+
7
+ @dataclass
8
+ class PeerAgentEntry:
9
+ name: str
10
+ url: str
11
+ status: str = "offline"
12
+ version: Optional[str] = None
13
+ capabilities: list = field(default_factory=list)
14
+ last_health_check: Optional[datetime] = None
15
+
16
+
17
+ _RESERVED_KEYS = {"health_check_interval"}
18
+
19
+
20
+ class VsPeerAgentRegistry:
21
+
22
+ _entries: Dict[str, PeerAgentEntry] = {}
23
+ _lock = threading.Lock()
24
+
25
+ @classmethod
26
+ def register(cls, name: str, url: str) -> None:
27
+ with cls._lock:
28
+ if name not in cls._entries:
29
+ cls._entries[name] = PeerAgentEntry(name=name, url=url)
30
+
31
+ @classmethod
32
+ def set_online(cls, name: str, version: Optional[str], capabilities: list) -> None:
33
+ with cls._lock:
34
+ if name in cls._entries:
35
+ entry = cls._entries[name]
36
+ entry.status = "online"
37
+ entry.version = version
38
+ entry.capabilities = capabilities
39
+ entry.last_health_check = datetime.now(timezone.utc)
40
+
41
+ @classmethod
42
+ def set_version_unchanged(cls, name: str) -> None:
43
+ with cls._lock:
44
+ if name in cls._entries:
45
+ entry = cls._entries[name]
46
+ entry.status = "online"
47
+ entry.last_health_check = datetime.now(timezone.utc)
48
+
49
+ @classmethod
50
+ def set_offline(cls, name: str) -> None:
51
+ with cls._lock:
52
+ if name in cls._entries:
53
+ entry = cls._entries[name]
54
+ entry.status = "offline"
55
+ entry.last_health_check = datetime.now(timezone.utc)
56
+
57
+ @classmethod
58
+ def get(cls, name: str) -> Optional[PeerAgentEntry]:
59
+ return cls._entries.get(name)
60
+
61
+ @classmethod
62
+ def get_available(cls) -> List[PeerAgentEntry]:
63
+ with cls._lock:
64
+ return [e for e in cls._entries.values() if e.status == "online"]
65
+
66
+ @classmethod
67
+ def get_all(cls) -> List[PeerAgentEntry]:
68
+ with cls._lock:
69
+ return list(cls._entries.values())
70
+
71
+ @classmethod
72
+ def parse_agents_config(cls, section: Dict[str, str]) -> Dict[str, str]:
73
+ return {k: v for k, v in section.items() if k not in _RESERVED_KEYS}
@@ -0,0 +1,34 @@
1
+ import asyncio
2
+ from typing import Optional
3
+
4
+ from vs_common.log.vs_log_manager import VsLogManager
5
+
6
+
7
+ class VsPeerAgentScheduler:
8
+
9
+ def __init__(self, health_interval: int = 30):
10
+ self._health_interval = health_interval
11
+ self._task: Optional[asyncio.Task] = None
12
+ self._logger = VsLogManager.get_instance(self.__class__.__name__)
13
+
14
+ async def start(self) -> None:
15
+ self._task = asyncio.create_task(self._health_loop())
16
+ self._logger.info(f"Peer agent scheduler started (interval={self._health_interval}s)")
17
+
18
+ async def stop(self) -> None:
19
+ if self._task:
20
+ self._task.cancel()
21
+ try:
22
+ await self._task
23
+ except asyncio.CancelledError:
24
+ pass
25
+ self._logger.info("Peer agent scheduler stopped")
26
+
27
+ async def _health_loop(self) -> None:
28
+ from vs_agent.discovery.vs_peer_capability_loader import VsPeerCapabilityLoader
29
+ while True:
30
+ await asyncio.sleep(self._health_interval)
31
+ try:
32
+ await VsPeerCapabilityLoader.check_all()
33
+ except Exception as e:
34
+ self._logger.error(f"Health check cycle failed: {e}", exc_info=True)
@@ -0,0 +1,81 @@
1
+ from typing import Dict
2
+
3
+ import httpx
4
+
5
+ from vs_agent.discovery.vs_peer_agent_registry import VsPeerAgentRegistry
6
+ from vs_common.log.vs_log_manager import VsLogManager
7
+
8
+ _logger = VsLogManager.get_instance("VsPeerCapabilityLoader")
9
+
10
+
11
+ class VsPeerCapabilityLoader:
12
+
13
+ @classmethod
14
+ async def load_all(cls, agents: Dict[str, str]) -> None:
15
+ _logger.info(f"Loading peer agents: {list(agents.keys())}")
16
+ for name, url in agents.items():
17
+ VsPeerAgentRegistry.register(name, url)
18
+ _logger.debug(f"[{name}] registered at {url}")
19
+ for name, url in agents.items():
20
+ await cls._check(name, url)
21
+ cls._log_summary()
22
+
23
+ @classmethod
24
+ async def check_all(cls) -> None:
25
+ _logger.debug("Running peer agent health check cycle")
26
+ for entry in VsPeerAgentRegistry.get_all():
27
+ await cls._check(entry.name, entry.url)
28
+ cls._log_summary()
29
+
30
+ @classmethod
31
+ async def _check(cls, name: str, url: str) -> None:
32
+ try:
33
+ async with httpx.AsyncClient(timeout=5.0) as client:
34
+ resp = await client.get(f"{url}/health")
35
+ resp.raise_for_status()
36
+ health = resp.json()
37
+
38
+ new_version = health.get("version")
39
+ entry = VsPeerAgentRegistry.get(name)
40
+ was_offline = entry and entry.status == "offline"
41
+
42
+ needs_capabilities = (
43
+ entry is None
44
+ or was_offline
45
+ or entry.version != new_version
46
+ or not entry.capabilities
47
+ )
48
+
49
+ if needs_capabilities:
50
+ reason = "came online" if was_offline else f"version changed {entry.version} → {new_version}" if entry and entry.version != new_version else "initial load"
51
+ _logger.info(f"[{name}] fetching capabilities ({reason})")
52
+ capabilities = await cls._fetch_capabilities(url)
53
+ VsPeerAgentRegistry.set_online(name, new_version, capabilities)
54
+ action_names = [a.get("name") for a in capabilities]
55
+ _logger.info(f"[{name}] online version={new_version} actions={action_names}")
56
+ else:
57
+ VsPeerAgentRegistry.set_version_unchanged(name)
58
+ _logger.debug(f"[{name}] online version={new_version} (no changes)")
59
+
60
+ except Exception as e:
61
+ entry = VsPeerAgentRegistry.get(name)
62
+ was_online = entry and entry.status == "online"
63
+ VsPeerAgentRegistry.set_offline(name)
64
+ if was_online:
65
+ _logger.warning(f"[{name}] went offline: {e}")
66
+ else:
67
+ _logger.debug(f"[{name}] still offline: {e}")
68
+
69
+ @classmethod
70
+ async def _fetch_capabilities(cls, url: str) -> list:
71
+ async with httpx.AsyncClient(timeout=10.0) as client:
72
+ resp = await client.get(f"{url}/capabilities")
73
+ resp.raise_for_status()
74
+ return resp.json().get("actions", [])
75
+
76
+ @classmethod
77
+ def _log_summary(cls) -> None:
78
+ all_entries = VsPeerAgentRegistry.get_all()
79
+ online = [e.name for e in all_entries if e.status == "online"]
80
+ offline = [e.name for e in all_entries if e.status == "offline"]
81
+ _logger.info(f"Peer agents — online: {online} offline: {offline}")
File without changes
@@ -0,0 +1,51 @@
1
+ import threading
2
+ from typing import Any, Callable, Dict, List, NamedTuple, Optional
3
+
4
+ from vs_agent.schema.vs_action_schema import Intent
5
+
6
+
7
+ class _ActionEntry(NamedTuple):
8
+ name: str
9
+ description: str
10
+ path: str
11
+ method: str
12
+ intents: List[Intent]
13
+ input_schema: Optional[Dict[str, Any]]
14
+ guards: List[Callable]
15
+ fn: Callable
16
+
17
+
18
+ class VsActionRegistry:
19
+ _registry: Dict[str, _ActionEntry] = {}
20
+ _lock: threading.Lock = threading.Lock()
21
+
22
+ @classmethod
23
+ def register(
24
+ cls,
25
+ name: str,
26
+ description: str,
27
+ path: str,
28
+ method: str,
29
+ intents: List[Intent],
30
+ input_schema: Optional[Dict[str, Any]],
31
+ guards: List[Callable],
32
+ fn: Callable,
33
+ ) -> None:
34
+ with cls._lock:
35
+ if name in cls._registry:
36
+ raise ValueError(f"Action '{name}' is already registered")
37
+ cls._registry[name] = _ActionEntry(
38
+ name=name,
39
+ description=description,
40
+ path=path,
41
+ method=method,
42
+ intents=intents,
43
+ input_schema=input_schema,
44
+ guards=guards,
45
+ fn=fn,
46
+ )
47
+
48
+ @classmethod
49
+ def get_all(cls) -> Dict[str, _ActionEntry]:
50
+ with cls._lock:
51
+ return dict(cls._registry)
File without changes
@@ -0,0 +1,21 @@
1
+ from typing import Any, Dict, List, Optional
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class Intent(BaseModel):
6
+ name: str
7
+ description: str
8
+ examples: Optional[List[str]] = None
9
+
10
+
11
+ class ActionCapability(BaseModel):
12
+ name: str
13
+ description: str
14
+ path: str
15
+ method: str
16
+ intents: List[Intent]
17
+ input_schema: Optional[Dict[str, Any]] = None
18
+
19
+
20
+ class CapabilitiesResponse(BaseModel):
21
+ actions: List[ActionCapability]
File without changes
@@ -0,0 +1,182 @@
1
+ import functools
2
+ import inspect
3
+ import threading
4
+ from typing import Optional
5
+
6
+ from vs_agent.registry.vs_action_registry import VsActionRegistry
7
+ from vs_common.config.vs_base_config import VsBaseConfig
8
+ from vs_common.log.vs_log_manager import VsLogManager
9
+ from vs_server.server.vs_server import VsServer
10
+
11
+
12
+ class VsAgentServer:
13
+
14
+ def __init__(
15
+ self,
16
+ config: VsBaseConfig,
17
+ http: Optional[str] = "fastapi",
18
+ mcp: Optional[str] = None,
19
+ ):
20
+ if not http and not mcp:
21
+ raise ValueError("At least one of 'http' or 'mcp' must be specified.")
22
+
23
+ self._config = config
24
+ self._logger = VsLogManager.get_instance(self.__class__.__name__)
25
+ self._http_server: Optional[VsServer] = None
26
+ self._mcp_server = None
27
+
28
+ if http:
29
+ from vs_server.factory.vs_server_factory import VsServerFactory
30
+ self._http_server = VsServerFactory.get(http, config)
31
+
32
+ if mcp:
33
+ from vs_mcp_agent.factory.vs_mcp_server_factory import VsMcpServerFactory
34
+ self._mcp_server = VsMcpServerFactory.get(mcp, config)
35
+
36
+ self._setup_peer_discovery(config)
37
+
38
+ # --- HTTP delegation ---
39
+
40
+ def add_controller(self, controller) -> "VsAgentServer":
41
+ self._require_http().add_controller(controller)
42
+ return self
43
+
44
+ def add_router(self, router) -> "VsAgentServer":
45
+ self._require_http().add_router(router)
46
+ return self
47
+
48
+ def add_websocket(self, handler) -> "VsAgentServer":
49
+ self._require_http().add_websocket(handler)
50
+ return self
51
+
52
+ def add_sse(self, handler) -> "VsAgentServer":
53
+ self._require_http().add_sse(handler)
54
+ return self
55
+
56
+ def get_app(self):
57
+ return self._require_http().get_app()
58
+
59
+ # --- MCP accessor ---
60
+
61
+ @property
62
+ def mcp(self):
63
+ if self._mcp_server is None:
64
+ raise RuntimeError("No MCP server configured. Pass mcp='fastmcp' to VsAgentServer.")
65
+ return self._mcp_server.mcp
66
+
67
+ # --- Actions (HTTP + MCP dual-registration) ---
68
+
69
+ def add_actions(self) -> "VsAgentServer":
70
+ from vs_agent.registry.vs_action_registry import VsActionRegistry
71
+
72
+ entries = VsActionRegistry.get_all()
73
+ if not entries:
74
+ return self
75
+
76
+ if self._http_server is not None:
77
+ self._register_http_actions(entries)
78
+
79
+ return self
80
+
81
+ def _register_http_actions(self, entries: dict) -> None:
82
+ from fastapi import APIRouter, Depends
83
+ from vs_agent.schema.vs_action_schema import ActionCapability, CapabilitiesResponse
84
+
85
+ security_deps = []
86
+ auth_enabled = self._config.get("agent.auth_enabled", default=True, data_type=bool)
87
+ if auth_enabled:
88
+ try:
89
+ from fastapi.security import HTTPBearer
90
+ from vs_security.guard.vs_security import VsSecurity
91
+ security_deps = [Depends(VsSecurity.from_manager()), Depends(HTTPBearer())]
92
+ except Exception:
93
+ pass
94
+
95
+ router = APIRouter()
96
+
97
+ @router.get("/capabilities")
98
+ async def capabilities() -> CapabilitiesResponse:
99
+ return CapabilitiesResponse(actions=[
100
+ ActionCapability(
101
+ name=e.name,
102
+ description=e.description,
103
+ path=e.path,
104
+ method=e.method,
105
+ intents=e.intents,
106
+ input_schema=e.input_schema,
107
+ )
108
+ for e in VsActionRegistry.get_all().values()
109
+ ])
110
+
111
+ for entry in entries.values():
112
+ fn = entry.fn
113
+ if entry.guards:
114
+ @functools.wraps(fn)
115
+ async def _handler(*args, _fn=fn, _guards=entry.guards, **kwargs):
116
+ for guard in _guards:
117
+ await guard()
118
+ return await _fn(*args, **kwargs)
119
+ _handler.__signature__ = inspect.signature(fn)
120
+ handler = _handler
121
+ else:
122
+ handler = fn
123
+
124
+ guard_deps = [Depends(g) for g in entry.guards]
125
+ getattr(router, entry.method.lower())(
126
+ entry.path,
127
+ dependencies=security_deps + guard_deps,
128
+ )(handler)
129
+
130
+ base_url = self._config.get("server.base_url", default="")
131
+ self._http_server.add_router(router, prefix=base_url)
132
+
133
+ # --- Run ---
134
+
135
+ def run(self) -> None:
136
+ if self._http_server and self._mcp_server:
137
+ mcp_thread = threading.Thread(target=self._mcp_server.run, daemon=True)
138
+ mcp_thread.start()
139
+ self._logger.info(f"[MCP] listening on port {self._mcp_server._config.port}")
140
+ self._logger.info(f"[HTTP] listening on port {self._config.get('server.port', 8000, int)}")
141
+ self._http_server.run()
142
+ elif self._mcp_server:
143
+ self._mcp_server.run()
144
+ else:
145
+ self._http_server.run()
146
+
147
+ # --- Peer discovery ---
148
+
149
+ def _setup_peer_discovery(self, config: VsBaseConfig) -> None:
150
+ agents_section = config.get_section("agents")
151
+ if not agents_section:
152
+ return
153
+
154
+ from vs_agent.discovery.vs_peer_agent_registry import VsPeerAgentRegistry
155
+ from vs_agent.discovery.vs_peer_capability_loader import VsPeerCapabilityLoader
156
+ from vs_agent.discovery.vs_peer_agent_scheduler import VsPeerAgentScheduler
157
+ from vs_agent.client.vs_peer_agent_client import VsPeerAgentClient
158
+ from vs_server.lifecycle.vs_lifecycle import startup, shutdown
159
+
160
+ agents = VsPeerAgentRegistry.parse_agents_config(agents_section)
161
+ health_interval = int(agents_section.get("health_check_interval", 30))
162
+
163
+ scheduler = VsPeerAgentScheduler(health_interval=health_interval)
164
+ VsPeerAgentClient.init(config)
165
+
166
+ @startup
167
+ async def _peer_discovery_start():
168
+ await VsPeerCapabilityLoader.load_all(agents)
169
+ await scheduler.start()
170
+
171
+ @shutdown
172
+ async def _peer_discovery_stop():
173
+ await scheduler.stop()
174
+
175
+ self._logger.info(f"Peer discovery enabled for agents: {list(agents.keys())}")
176
+
177
+ # --- Internal ---
178
+
179
+ def _require_http(self) -> VsServer:
180
+ if self._http_server is None:
181
+ raise RuntimeError("No HTTP server configured. Pass http='fastapi' to VsAgentServer.")
182
+ return self._http_server
@@ -0,0 +1,692 @@
1
+ Metadata-Version: 2.4
2
+ Name: vs-agent
3
+ Version: 0.1.1
4
+ Summary: Full-stack agent framework for Viveka Sutra — HTTP + MCP server in one
5
+ Project-URL: Homepage, https://vivekasutra.com/
6
+ Project-URL: Source, https://github.com/vivekasutra/viveka-mula
7
+ Keywords: agent,mcp,fastapi,http,viveka,vs
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: Other/Proprietary License
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Framework :: AsyncIO
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: vs-common
20
+ Requires-Dist: vs-server
21
+ Provides-Extra: mcp
22
+ Requires-Dist: vs-mcp-agent; extra == "mcp"
23
+ Provides-Extra: security
24
+ Requires-Dist: vs-security; extra == "security"
25
+ Provides-Extra: dev
26
+ Requires-Dist: build; extra == "dev"
27
+ Requires-Dist: twine; extra == "dev"
28
+ Requires-Dist: pytest>=8.0; extra == "dev"
29
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
30
+
31
+ # vs-agent
32
+
33
+ Full-stack agent framework for Viveka Sutra — build agents that serve over HTTP, MCP, or both, from a single codebase.
34
+
35
+ ---
36
+
37
+ ## Overview
38
+
39
+ `vs-agent` combines `vs-server` (HTTP/WebSocket/SSE) and `vs-mcp-agent` (MCP) into a single `VsAgentServer`. It lets you expose a capability once with `@action` and have it available simultaneously as an HTTP endpoint and an MCP tool — with the same auth, guards, and business logic.
40
+
41
+ The library is fully server-agnostic. `VsAgentServer` does not hardcode FastAPI or FastMCP — it uses `VsServerFactory` and `VsMcpServerFactory` to resolve the correct server implementation at startup. Swapping or adding a new server type requires no changes to application code.
42
+
43
+ ---
44
+
45
+ ## The Problem It Solves
46
+
47
+ An agent needs to be callable by both humans (via HTTP) and AI clients (via MCP). Without `vs-agent`, you register the same function twice, apply guards twice, and maintain two sets of route definitions.
48
+
49
+ ### Without vs-agent
50
+
51
+ ```python
52
+ # HTTP route
53
+ @router.post("/v1/docs/search")
54
+ async def search_docs_http(query: str, auth=Depends(require_auth)):
55
+ return await _search(query)
56
+
57
+ # MCP tool — separate registration, separate guard wiring
58
+ @mcp.tool(name="search_docs")
59
+ async def search_docs_mcp(query: str) -> dict:
60
+ await require_auth()
61
+ return await _search(query)
62
+ ```
63
+
64
+ ### With vs-agent
65
+
66
+ ```python
67
+ @action(
68
+ name="search_docs",
69
+ description="Search VS library documentation",
70
+ path="/v1/docs/search",
71
+ guards=[VsActionSecurity(roles=["user"])],
72
+ )
73
+ async def search_docs(query: str) -> VsToolResponse:
74
+ return VsToolResponse(status="success", result=await _search(query))
75
+ ```
76
+
77
+ One function, one guard, available on both protocols.
78
+
79
+ ---
80
+
81
+ ## Installation
82
+
83
+ ```bash
84
+ pip install vs-agent
85
+ ```
86
+
87
+ With MCP support:
88
+
89
+ ```bash
90
+ pip install vs-agent[mcp]
91
+ ```
92
+
93
+ With auth guard support:
94
+
95
+ ```bash
96
+ pip install vs-agent[mcp,security]
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Dependencies
102
+
103
+ | Library | Required | Purpose |
104
+ |---|---|---|
105
+ | `vs-common` | Yes | Config, logging |
106
+ | `vs-server` | Yes | HTTP/WebSocket/SSE server |
107
+ | `vs-mcp-agent` | No — install with `[mcp]` extra | MCP server |
108
+ | `vs-security` | No — install with `[security]` extra | JWT auth and `VsActionSecurity` guard |
109
+
110
+ ---
111
+
112
+ ## Configuration
113
+
114
+ `VsAgentServer` reads HTTP config via `vs-server` and MCP config via `vs-mcp-agent`. Both use the same `config.ini` file — separated by section.
115
+
116
+ **HTTP section (from `vs-server`):**
117
+
118
+ | Key | Default | Description |
119
+ |---|---|---|
120
+ | `server.name` | `vs-agent` | Server name shown in logs and `/` response |
121
+ | `server.version` | `0.1.0` | Version shown in logs and `/` response |
122
+ | `server.host` | `0.0.0.0` | Host to bind |
123
+ | `server.port` | `8000` | HTTP port |
124
+ | `server.reload` | `false` | Enable hot reload (dev only) |
125
+ | `server.workers` | `1` | Number of worker processes |
126
+ | `server.cors_origins` | `*` | Comma-separated allowed CORS origins |
127
+ | `server.ssl_certfile` | — | Path to TLS certificate file |
128
+ | `server.ssl_keyfile` | — | Path to TLS private key file |
129
+
130
+ **MCP section (from `vs-mcp-agent`):**
131
+
132
+ | Key | Default | Description |
133
+ |---|---|---|
134
+ | `agent.name` | `vs-agent` | MCP server name |
135
+ | `agent.version` | `0.1.0` | MCP server version |
136
+ | `agent.transport` | `streamable-http` | Transport: `stdio`, `sse`, `streamable-http` |
137
+ | `agent.host` | `0.0.0.0` | MCP host |
138
+ | `agent.port` | `8080` | MCP port |
139
+ | `agent.auth_enabled` | `false` | Enable JWT auth middleware on MCP |
140
+
141
+ **`config.ini` example:**
142
+
143
+ ```ini
144
+ [server]
145
+ name = my-agent
146
+ version = 1.0.0
147
+ host = 0.0.0.0
148
+ port = 8000
149
+
150
+ [agent]
151
+ name = my-agent
152
+ version = 1.0.0
153
+ transport = streamable-http
154
+ host = 0.0.0.0
155
+ port = 8080
156
+ auth_enabled = true
157
+
158
+ [auth]
159
+ secret_key = your-secret-key
160
+ algorithm = HS256
161
+
162
+ [logging]
163
+ level = INFO
164
+ file_path = ./logs/agent.log
165
+ ```
166
+
167
+ ---
168
+
169
+ ## Quick Start
170
+
171
+ ### HTTP + MCP (most common)
172
+
173
+ ```python
174
+ from vs_common.config.vs_ini_config import VsIniConfig
175
+ from vs_common.log.vs_log_manager import VsLogManager
176
+ from vs_common.schema.vs_log_config import VsLogConfig
177
+ from vs_server.server.vs_fast_api_server import VsFastApiServer # noqa — auto-registers "fastapi"
178
+ from vs_mcp_agent.server.vs_fast_mcp_server import VsFastMcpServer # noqa — auto-registers "fastmcp"
179
+ from vs_agent.server.vs_agent_server import VsAgentServer
180
+
181
+ import my_agent.actions # noqa — registers @action functions
182
+
183
+
184
+ def main():
185
+ config = VsIniConfig("config.ini")
186
+ VsLogManager.init(VsLogConfig(level=config.get("logging.level", default="INFO")))
187
+
188
+ server = VsAgentServer(config, http="fastapi", mcp="fastmcp")
189
+ server.add_actions()
190
+ server.run()
191
+
192
+
193
+ if __name__ == "__main__":
194
+ main()
195
+ ```
196
+
197
+ ### HTTP only
198
+
199
+ ```python
200
+ server = VsAgentServer(config, http="fastapi")
201
+ server.add_actions()
202
+ server.run()
203
+ ```
204
+
205
+ ### MCP only
206
+
207
+ ```python
208
+ from vs_mcp_agent.server.vs_fast_mcp_server import VsFastMcpServer # noqa — auto-registers "fastmcp"
209
+
210
+ server = VsAgentServer(config, mcp="fastmcp")
211
+ server.run()
212
+ ```
213
+
214
+ ---
215
+
216
+ ## How It All Fits Together
217
+
218
+ ```
219
+ Application Startup
220
+ └── import VsFastApiServer # auto-registers "fastapi" into VsServerFactory
221
+ └── import VsFastMcpServer # auto-registers "fastmcp" into VsMcpServerFactory
222
+ └── import my_agent.actions # @action decorators self-register into VsActionRegistry + VsToolRegistry
223
+
224
+ VsAgentServer(config, http="fastapi", mcp="fastmcp")
225
+ ├── VsServerFactory.get("fastapi", config) → VsFastApiServer
226
+ └── VsMcpServerFactory.get("fastmcp", config) → VsFastMcpServer
227
+
228
+ server.add_actions()
229
+ ├── reads VsActionRegistry → wires HTTP routes + /capabilities
230
+ └── VsFastMcpServer already has tools from VsToolRegistry (wired at run())
231
+
232
+ server.run()
233
+ ├── MCP server starts on background thread (port 8080)
234
+ └── HTTP server starts on main thread (port 8000)
235
+ ```
236
+
237
+ ---
238
+
239
+ ## VsAgentServer
240
+
241
+ `VsAgentServer` is the central coordinator. It creates and manages HTTP and MCP server instances via their respective factories.
242
+
243
+ ```python
244
+ from vs_agent.server.vs_agent_server import VsAgentServer
245
+
246
+ server = VsAgentServer(config, http="fastapi", mcp="fastmcp")
247
+ ```
248
+
249
+ **Constructor parameters:**
250
+
251
+ | Parameter | Type | Default | Description |
252
+ |---|---|---|---|
253
+ | `config` | `VsBaseConfig` | — | Application config |
254
+ | `http` | `Optional[str]` | `"fastapi"` | HTTP server key. Pass `None` for MCP-only mode. |
255
+ | `mcp` | `Optional[str]` | `None` | MCP server key. Pass `"fastmcp"` to enable MCP. |
256
+
257
+ At least one of `http` or `mcp` must be specified — both `None` raises `ValueError`.
258
+
259
+ **Methods:**
260
+
261
+ | Method | Description |
262
+ |---|---|
263
+ | `add_controller(controller)` | Register an HTTP `@controller` class. Delegates to the HTTP server. |
264
+ | `add_router(router)` | Register a raw router. Delegates to the HTTP server. |
265
+ | `add_websocket(handler)` | Register a `@websocket` handler. Delegates to the HTTP server. |
266
+ | `add_sse(handler)` | Register an `@sse` handler. Delegates to the HTTP server. |
267
+ | `add_actions()` | Wire all `@action` functions to HTTP routes and expose `/capabilities`. |
268
+ | `get_app()` | Return the underlying ASGI app (e.g. FastAPI instance). |
269
+ | `run()` | Start the server(s). MCP runs on a background daemon thread; HTTP runs on the main thread. |
270
+
271
+ **`mcp` property:**
272
+
273
+ ```python
274
+ server.mcp # returns the underlying FastMCP instance for advanced configuration
275
+ ```
276
+
277
+ Raises `RuntimeError` if no MCP server is configured.
278
+
279
+ **All `add_*` methods return `self` for chaining:**
280
+
281
+ ```python
282
+ server = (
283
+ VsAgentServer(config, http="fastapi", mcp="fastmcp")
284
+ .add_controller(HealthController())
285
+ .add_actions()
286
+ )
287
+ server.run()
288
+ ```
289
+
290
+ ---
291
+
292
+ ## @action
293
+
294
+ Registers a function simultaneously as:
295
+ - An MCP tool — in `VsToolRegistry`, picked up by `VsFastMcpServer` when `run()` is called
296
+ - An HTTP endpoint — in `VsActionRegistry`, wired by `server.add_actions()`
297
+
298
+ ```python
299
+ from vs_agent.decorator.vs_action_decorator import action
300
+ from vs_mcp_agent.schema.vs_tool_response import VsToolResponse
301
+
302
+ @action(
303
+ name="search_docs",
304
+ description="Search VS library documentation",
305
+ path="/v1/docs/search",
306
+ method="POST",
307
+ guards=[VsActionSecurity(roles=["user"])],
308
+ )
309
+ async def search_docs(query: str) -> VsToolResponse:
310
+ results = await _do_search(query)
311
+ return VsToolResponse(status="success", result=results, summary="Search complete")
312
+ ```
313
+
314
+ **Parameters:**
315
+
316
+ | Parameter | Type | Required | Default | Description |
317
+ |---|---|---|---|---|
318
+ | `name` | `str` | Yes | — | MCP tool name and action identifier |
319
+ | `description` | `str` | Yes | — | Shown in MCP tool list and `/capabilities` |
320
+ | `path` | `str` | Yes | — | HTTP endpoint path |
321
+ | `method` | `str` | No | `"POST"` | HTTP method (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) |
322
+ | `intents` | `List[Intent]` | No | `[]` | Semantic intents for agent routing |
323
+ | `input_schema` | `Dict[str, Any]` | No | `None` | JSON schema for the action input |
324
+ | `guards` | `List[Callable]` | No | `[]` | Guards applied on both HTTP and MCP |
325
+
326
+ **Import order matters.** `@action` self-registers at import time into both `VsActionRegistry` and `VsToolRegistry`. Import your action modules before calling `server.add_actions()` or `server.run()`.
327
+
328
+ ```python
329
+ import my_agent.actions.search # noqa — triggers @action registration
330
+ import my_agent.actions.summary # noqa
331
+
332
+ server.add_actions()
333
+ server.run()
334
+ ```
335
+
336
+ ---
337
+
338
+ ## Authorization with VsActionSecurity
339
+
340
+ `VsActionSecurity` is a unified guard that works on both HTTP and MCP. It reads the auth context set by `VsSecurityFactory` (HTTP) or `VsMcpAuthMiddleware` (MCP).
341
+
342
+ ```python
343
+ from vs_agent.auth.vs_action_security import VsActionSecurity
344
+
345
+ @action(
346
+ name="search_docs",
347
+ description="Search documentation",
348
+ path="/v1/docs/search",
349
+ guards=[VsActionSecurity(roles=["user"])],
350
+ )
351
+ async def search_docs(query: str) -> VsToolResponse:
352
+ ...
353
+ ```
354
+
355
+ **No roles (auth only):**
356
+
357
+ ```python
358
+ guards=[VsActionSecurity()] # rejects unauthenticated callers, any role allowed
359
+ ```
360
+
361
+ **Multiple roles (any match):**
362
+
363
+ ```python
364
+ guards=[VsActionSecurity(roles=["admin", "editor"])] # passes if caller has admin OR editor
365
+ ```
366
+
367
+ **Multiple guards (all must pass, in order):**
368
+
369
+ ```python
370
+ guards=[VsActionSecurity(roles=["admin"]), require_verified_account]
371
+ ```
372
+
373
+ **How guards run per protocol:**
374
+
375
+ | Protocol | Auth context source | Guard execution |
376
+ |---|---|---|
377
+ | HTTP | `VsSecurityFactory.get()` sets context via FastAPI `Depends` | Guards called in order after auth context is set |
378
+ | MCP | `VsMcpAuthMiddleware` sets context before tool dispatch | Guards called in order before tool function executes |
379
+
380
+ ---
381
+
382
+ ## Lifecycle Hooks
383
+
384
+ Use lifecycle hooks to run code at server startup and shutdown — creating DB tables, warming caches, closing connections, etc.
385
+
386
+ ### HTTP lifecycle (`vs-server`)
387
+
388
+ ```python
389
+ from vs_server.lifecycle.vs_lifecycle import startup, shutdown
390
+
391
+ @startup
392
+ async def warm_cache():
393
+ await VsCacheManager.set("ready", True)
394
+
395
+ @shutdown
396
+ async def flush_cache():
397
+ await VsCacheManager.delete("ready")
398
+ ```
399
+
400
+ Register hook modules with `server_registry`:
401
+
402
+ ```python
403
+ from vs_server.decorator.vs_server_registry import server_registry
404
+
405
+ @server_registry(hooks="my_agent.lifecycle")
406
+ def main():
407
+ ...
408
+ ```
409
+
410
+ ### MCP lifecycle (`vs-mcp-agent`)
411
+
412
+ ```python
413
+ from vs_mcp_agent.lifecycle.vs_mcp_lifecycle import mcp_startup, mcp_shutdown
414
+
415
+ @mcp_startup
416
+ async def init_mcp_resources():
417
+ await load_tool_index()
418
+
419
+ @mcp_shutdown
420
+ async def cleanup_mcp_resources():
421
+ await close_tool_connections()
422
+ ```
423
+
424
+ Register hook modules with `mcp_server_registry`:
425
+
426
+ ```python
427
+ from vs_mcp_agent.decorator.vs_mcp_server_registry import mcp_server_registry
428
+
429
+ @mcp_server_registry(hooks="my_agent.mcp_lifecycle")
430
+ def main():
431
+ ...
432
+ ```
433
+
434
+ When both are used, HTTP and MCP hooks run independently — HTTP hooks fire when the HTTP server starts/stops, MCP hooks fire when the MCP server starts/stops.
435
+
436
+ ---
437
+
438
+ ## HTTP-only Capabilities
439
+
440
+ For capabilities that should only be available over HTTP, use `@controller` from `vs-server` directly and register via `server.add_controller()`:
441
+
442
+ ```python
443
+ from vs_server.decorator.vs_controller_decorator import controller, get, post
444
+
445
+ @controller("/v1/internal")
446
+ class InternalController:
447
+
448
+ @get("/status")
449
+ async def status(self):
450
+ return {"status": "ok"}
451
+
452
+ server.add_controller(InternalController())
453
+ ```
454
+
455
+ ---
456
+
457
+ ## MCP-only Capabilities
458
+
459
+ For capabilities that should only be available over MCP, use `@tool` from `vs-mcp-agent` directly:
460
+
461
+ ```python
462
+ from vs_mcp_agent.decorator.tool import tool
463
+ from vs_mcp_agent.schema.vs_tool_response import VsToolResponse
464
+
465
+ @tool(name="internal_tool", description="Internal MCP tool only")
466
+ async def internal_tool(query: str) -> VsToolResponse:
467
+ ...
468
+ ```
469
+
470
+ These are picked up automatically by `VsFastMcpServer` at `run()` — no extra registration needed.
471
+
472
+ ---
473
+
474
+ ## /capabilities Endpoint
475
+
476
+ `server.add_actions()` automatically registers a `GET /capabilities` endpoint on the HTTP server. It returns all registered actions with their metadata — used by orchestrators to discover what the agent can do.
477
+
478
+ **Response:**
479
+
480
+ ```json
481
+ {
482
+ "actions": [
483
+ {
484
+ "name": "search_docs",
485
+ "description": "Search VS library documentation",
486
+ "path": "/v1/docs/search",
487
+ "method": "POST",
488
+ "intents": [],
489
+ "input_schema": null
490
+ }
491
+ ]
492
+ }
493
+ ```
494
+
495
+ ---
496
+
497
+ ## Intents
498
+
499
+ `Intent` gives each action semantic labels that orchestrators use for routing — matching a user request to the right action without exact string matching.
500
+
501
+ ```python
502
+ from vs_agent.schema.vs_action_schema import Intent
503
+
504
+ @action(
505
+ name="search_docs",
506
+ description="Search VS library documentation",
507
+ path="/v1/docs/search",
508
+ intents=[
509
+ Intent(
510
+ name="search",
511
+ description="Find documentation matching a query",
512
+ examples=["how do I configure logging", "what does VsLogManager do"],
513
+ )
514
+ ],
515
+ )
516
+ async def search_docs(query: str) -> VsToolResponse:
517
+ ...
518
+ ```
519
+
520
+ Intents are returned in `/capabilities` and are available as metadata on `VsActionRegistry` entries.
521
+
522
+ ---
523
+
524
+ ## Error Handling
525
+
526
+ Exceptions from `@action` functions propagate through both protocols:
527
+ - On HTTP, `vs-server`'s exception handlers convert them to HTTP responses.
528
+ - On MCP, `VsFastMcpServer` returns an error result to the MCP client.
529
+
530
+ Use exceptions from `vs-server` for standard HTTP error semantics — they are handled automatically:
531
+
532
+ ```python
533
+ from vs_server.schema.exceptions import NotFoundException, ServiceUnavailableException
534
+
535
+ @action(name="get_doc", description="Get a document", path="/v1/docs/{doc_id}")
536
+ async def get_doc(doc_id: str) -> VsToolResponse:
537
+ doc = await repo.find(doc_id)
538
+ if doc is None:
539
+ raise NotFoundException(f"Document '{doc_id}' not found")
540
+ return VsToolResponse(status="success", result=doc)
541
+ ```
542
+
543
+ ---
544
+
545
+ ## Class Reference
546
+
547
+ ---
548
+
549
+ ### VsAgentServer
550
+
551
+ Coordinates HTTP and MCP servers. Uses `VsServerFactory` and `VsMcpServerFactory` to resolve server implementations. Supports HTTP-only, MCP-only, or HTTP + MCP modes.
552
+
553
+ **Constructor:**
554
+
555
+ | Parameter | Type | Default | Description |
556
+ |---|---|---|---|
557
+ | `config` | `VsBaseConfig` | — | Application config |
558
+ | `http` | `Optional[str]` | `"fastapi"` | HTTP server key registered in `VsServerFactory`. Pass `None` for MCP-only. |
559
+ | `mcp` | `Optional[str]` | `None` | MCP server key registered in `VsMcpServerFactory`. Pass `"fastmcp"` to enable MCP. |
560
+
561
+ **Methods:**
562
+
563
+ | Method | Signature | Description |
564
+ |---|---|---|
565
+ | `add_controller` | `add_controller(controller) -> VsAgentServer` | Register an HTTP controller. Requires `http` mode. |
566
+ | `add_router` | `add_router(router) -> VsAgentServer` | Register a raw router. Requires `http` mode. |
567
+ | `add_websocket` | `add_websocket(handler) -> VsAgentServer` | Register a WebSocket handler. Requires `http` mode. |
568
+ | `add_sse` | `add_sse(handler) -> VsAgentServer` | Register an SSE handler. Requires `http` mode. |
569
+ | `add_actions` | `add_actions() -> VsAgentServer` | Wire all `@action` functions to HTTP routes and `/capabilities`. |
570
+ | `get_app` | `get_app() -> Any` | Return the underlying ASGI app. Requires `http` mode. |
571
+ | `run` | `run() -> None` | Start all servers. MCP on daemon thread, HTTP on main thread. |
572
+
573
+ **Property:**
574
+
575
+ | Property | Type | Description |
576
+ |---|---|---|
577
+ | `mcp` | `Any` | The underlying FastMCP instance. Raises `RuntimeError` if MCP not configured. |
578
+
579
+ **Notes:**
580
+ - All `add_*` methods call `_require_http()` internally and raise `RuntimeError` if `http=None`.
581
+ - In HTTP + MCP mode, `run()` starts MCP on a background daemon thread then blocks on the HTTP server.
582
+ - Import `VsFastApiServer` before constructing `VsAgentServer` to ensure `"fastapi"` is registered.
583
+ - Import `VsFastMcpServer` before constructing `VsAgentServer` to ensure `"fastmcp"` is registered.
584
+
585
+ ---
586
+
587
+ ### `@action`
588
+
589
+ Decorator. Registers a function as both an MCP tool (via `VsToolRegistry`) and an HTTP action (via `VsActionRegistry`). Self-registers at import time.
590
+
591
+ **Parameters:**
592
+
593
+ | Parameter | Type | Required | Default | Description |
594
+ |---|---|---|---|---|
595
+ | `name` | `str` | Yes | — | MCP tool name and action key |
596
+ | `description` | `str` | Yes | — | Shown in MCP tool list and `/capabilities` |
597
+ | `path` | `str` | Yes | — | HTTP endpoint path |
598
+ | `method` | `str` | No | `"POST"` | HTTP method |
599
+ | `intents` | `List[Intent]` | No | `[]` | Semantic intents for orchestrator routing |
600
+ | `input_schema` | `Dict[str, Any]` | No | `None` | JSON schema for the action input |
601
+ | `guards` | `List[Callable]` | No | `[]` | Applied on both HTTP and MCP, in order |
602
+
603
+ **Notes:**
604
+ - Each `name` must be unique across all `@action` registrations. Duplicate names raise `ValueError`.
605
+ - `method` is HTTP-only — MCP tools have no HTTP method concept.
606
+ - `guards` must be async callables.
607
+
608
+ ---
609
+
610
+ ### VsActionSecurity
611
+
612
+ Guard class for unified HTTP + MCP authorization. Reads from the auth context set by whichever protocol is active.
613
+
614
+ **Constructor:**
615
+
616
+ | Parameter | Type | Default | Description |
617
+ |---|---|---|---|
618
+ | `roles` | `Optional[List[str]]` | `[]` | Required roles. Caller must have at least one. Empty list = any authenticated caller. |
619
+
620
+ **Behaviour:**
621
+
622
+ | Condition | Result |
623
+ |---|---|
624
+ | No auth context | Raises `PermissionError("Unauthenticated request")` |
625
+ | Auth context present, no roles required | Passes |
626
+ | Auth context present, caller has required role | Passes |
627
+ | Auth context present, caller lacks required role | Raises `PermissionError` |
628
+
629
+ **Notes:**
630
+ - On HTTP, auth context is set by `VsSecurityFactory.get()` (from `vs-security`).
631
+ - On MCP, auth context is set by `VsMcpAuthMiddleware` (from `vs-mcp-agent`).
632
+ - `VsActionSecurity` reads from the same context variable regardless of protocol.
633
+
634
+ ---
635
+
636
+ ### VsActionRegistry
637
+
638
+ Class-level registry of all `@action`-registered functions. Thread-safe.
639
+
640
+ **Methods:**
641
+
642
+ | Method | Signature | Description |
643
+ |---|---|---|
644
+ | `register` | `register(name, description, path, method, intents, input_schema, guards, fn) -> None` | Register an action. Raises `ValueError` if name is already registered. |
645
+ | `get_all` | `get_all() -> Dict[str, _ActionEntry]` | Returns a snapshot of all registered actions. |
646
+
647
+ **Notes:**
648
+ - `@action` calls `VsActionRegistry.register()` and `VsToolRegistry.register_fn()` at decoration time.
649
+ - `server.add_actions()` reads from `VsActionRegistry.get_all()` to wire HTTP routes.
650
+
651
+ ---
652
+
653
+ ### Intent
654
+
655
+ Pydantic model. Semantic label for an action — used by orchestrators for routing.
656
+
657
+ **Fields:**
658
+
659
+ | Field | Type | Required | Description |
660
+ |---|---|---|---|
661
+ | `name` | `str` | Yes | Short intent identifier |
662
+ | `description` | `str` | Yes | What this intent means |
663
+ | `examples` | `Optional[List[str]]` | No | Example user phrases that trigger this intent |
664
+
665
+ ---
666
+
667
+ ### ActionCapability
668
+
669
+ Pydantic model. Metadata for a single action as returned by `/capabilities`.
670
+
671
+ **Fields:**
672
+
673
+ | Field | Type | Description |
674
+ |---|---|---|
675
+ | `name` | `str` | Action name |
676
+ | `description` | `str` | Action description |
677
+ | `path` | `str` | HTTP endpoint path |
678
+ | `method` | `str` | HTTP method |
679
+ | `intents` | `List[Intent]` | Semantic intents |
680
+ | `input_schema` | `Optional[Dict[str, Any]]` | JSON schema for input |
681
+
682
+ ---
683
+
684
+ ### CapabilitiesResponse
685
+
686
+ Pydantic model. Response from `GET /capabilities`.
687
+
688
+ **Fields:**
689
+
690
+ | Field | Type | Description |
691
+ |---|---|---|
692
+ | `actions` | `List[ActionCapability]` | All registered actions |
@@ -0,0 +1,21 @@
1
+ vs_agent/__init__.py,sha256=3yuN4M5I9y2QMcXQ5hhqE73XwH2O97mIH3QaJ-acnS4,94
2
+ vs_agent/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ vs_agent/auth/vs_action_security.py,sha256=jj6Hb8_CNO_1lCYoz9WsbfiJQ_V4ifz9YoqAoeoYYYw,502
4
+ vs_agent/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ vs_agent/client/vs_peer_agent_client.py,sha256=i_62CO4d-fLr47cqnLQc1RIMZX5I5DBOJKc6hTVscr0,2404
6
+ vs_agent/decorator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ vs_agent/decorator/vs_action_decorator.py,sha256=MM2Wk9vsLaTV8VtEBVBrcXH240oV5oWsWw5rGVLi2lY,801
8
+ vs_agent/discovery/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ vs_agent/discovery/vs_peer_agent_registry.py,sha256=xuiiJPNcogWmr51S8qRB_2N1DfXbhDGfU7VT7wzGhN4,2284
10
+ vs_agent/discovery/vs_peer_agent_scheduler.py,sha256=aGfOePuTjEezZitCdr8r3HT251wTMETXgwLrq2EuSAU,1207
11
+ vs_agent/discovery/vs_peer_capability_loader.py,sha256=_BK0vsRTgZh6hgfN2klpvCZBZDaDCZmTtGjZoiBk-8E,3304
12
+ vs_agent/registry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ vs_agent/registry/vs_action_registry.py,sha256=agQ43fofEAwkWtjX9UZe5wLpgFO4GVoOKS8mZR1e-xo,1331
14
+ vs_agent/schema/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ vs_agent/schema/vs_action_schema.py,sha256=Se0XX3kYmDylYok2cn3yJ9jVPCjdXWrFIq6Va45nt64,434
16
+ vs_agent/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ vs_agent/server/vs_agent_server.py,sha256=nVeUmkcfGnI1bz9VFJ_HP7-NFymxHjsWMdfuZbEwya4,6397
18
+ vs_agent-0.1.1.dist-info/METADATA,sha256=j2ZsFytzlrpFaG-AG2LSbjOM9j5Q8i0IIUAoiGXa5Sc,21506
19
+ vs_agent-0.1.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
20
+ vs_agent-0.1.1.dist-info/top_level.txt,sha256=lzOLrUrjfBxn4S1Evc_osgkXJpPbw62uonkfPrfx6uM,9
21
+ vs_agent-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ vs_agent