ms-tau-sdk 2.0.13.dev55__tar.gz → 2.0.14.dev57__tar.gz
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.
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/CHANGELOG.md +18 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/PKG-INFO +1 -1
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/pyproject.toml +1 -1
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/agent_skills/tau_project_customization/SKILL.md +10 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/backend/mcp.py +125 -13
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/tools/mcp_applications.py +260 -10
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/.gitignore +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/README.md +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/__init__.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/app.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/cli.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/config.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/env_file.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/logs.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/proxy.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/state.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/static/BULMA-LICENSE.txt +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/static/app.js +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/static/board.css +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/static/bulma.min.css +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/static/index.html +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/__init__.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/agent_skills/tau_a2a_runtime_adapter/SKILL.md +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/agent_skills/tau_local_development/SKILL.md +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/agent_skills/tau_repository_integration/SKILL.md +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/agent_skills/tau_security_and_access/SKILL.md +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/api/__init__.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/api/a2a.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/api/chat.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/api/conversations.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/api/dependencies.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/api/health.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/api/inspection.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/api/local_chat.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/api/models.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/api/request_identity.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/api/sessions.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/app.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/application.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/backend/__init__.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/backend/assertions.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/backend/auth.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/backend/client.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/backend/local.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/backend/models.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/backend/routes.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/cli.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/errors.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/logging.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/protocols/__init__.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/protocols/a2a_failure.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/protocols/a2a_roles.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/protocols/chat_history.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/providers/__init__.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/providers/definitions.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/providers/factory.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/providers/tau_compat.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/resources/__init__.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/resources/loader.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/resources/prompts/review-code-repository.md +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/runtime/__init__.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/runtime/events.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/runtime/extensions.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/runtime/failures.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/runtime/live_turns.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/runtime/manager.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/runtime/observability.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/runtime/provenance.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/runtime/requester.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/runtime/session.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/runtime/task_context.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/sessions/__init__.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/sessions/storage.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/settings.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/skills.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/tools/__init__.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/tools/mainsequence_mcp.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/tools/mcp_connection.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/tools/skill_read.py +0 -0
- {ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/tools/task_control.py +0 -0
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- **An Agent reads the skills and resources its declared applications publish
|
|
6
|
+
([#98](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/98), ADR 0023).** An application
|
|
7
|
+
that published skills through the MCP Skills extension, as the mainsequence SDK's skills
|
|
8
|
+
publisher does, reached its Agent only through `<name>__list_tools` and `<name>__call_tool`: the
|
|
9
|
+
Agent could not discover or read the skills. Each declared application now also gets
|
|
10
|
+
`<name>__list_resources`, which lists its skills, each with its files, and its other resources,
|
|
11
|
+
and `<name>__read_resource`, which reads one of them by URI. `<name>__list_tools` says when the
|
|
12
|
+
application publishes them. An application's session first asks MCP's `server/discover` on
|
|
13
|
+
protocol `2026-07-28`, where the extension is declared, and opens an application that does not
|
|
14
|
+
answer it with `initialize`, as before; the platform's own MCP is unchanged. Only what the
|
|
15
|
+
application lists is read, and a skill file is returned only when its bytes match the size and
|
|
16
|
+
SHA-256 digest of the skill's list. Every read names the application. Published skills never
|
|
17
|
+
become the project's skills and grant nothing; with the coding tools, a model that follows one
|
|
18
|
+
can run commands, which the [Security and access guide](docs/reference/security-model.md#calling-other-applications)
|
|
19
|
+
now states.
|
|
20
|
+
|
|
3
21
|
## 2.0.13 — 2026-10-12
|
|
4
22
|
|
|
5
23
|
- **ms-tau-sdk installs the MCP Python SDK 2.x.** The `mcp` dependency moves from `mcp>=1.27,<2`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ms-tau-sdk
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.14.dev57
|
|
4
4
|
Summary: Workspace-bound Tau application primitives for Main Sequence projects
|
|
5
5
|
Project-URL: Changelog, https://github.com/mainsequence-sdk/ms-tau-sdk/blob/development/CHANGELOG.md
|
|
6
6
|
Project-URL: Documentation, https://github.com/mainsequence-sdk/ms-tau-sdk/tree/development/docs
|
|
@@ -96,6 +96,16 @@ retry guidance for up to `TAU_MCP_APPLICATION_READY_TIMEOUT_SECONDS` (default `1
|
|
|
96
96
|
turn time. This startup budget is separate from the tool's execution limit; an expired readiness
|
|
97
97
|
wait reports `readiness_timeout`. Cancellation, ended delegation and terminal refusals stop the wait.
|
|
98
98
|
|
|
99
|
+
## Skills that applications publish
|
|
100
|
+
|
|
101
|
+
A declared application can publish Agent Skills through the MCP Skills extension, as the
|
|
102
|
+
mainsequence SDK's skills publisher does. The Agent lists them with `<name>__list_resources` and
|
|
103
|
+
reads one file with `<name>__read_resource`; a skill file is returned only when its bytes match the
|
|
104
|
+
size and SHA-256 digest the skill lists. Published skills stay the application's: they are not
|
|
105
|
+
project skills under `.tau/skills/`, and fields such as `allowed-tools` grant nothing. With the
|
|
106
|
+
coding tools, a model that follows a published skill can run commands; set
|
|
107
|
+
`TAU_EXCLUDE_BASE_TOOLS=true` when that must not happen.
|
|
108
|
+
|
|
99
109
|
## Invariants extensions must not replace
|
|
100
110
|
|
|
101
111
|
Extensions may change effective agent capabilities, but they must not replace or bypass:
|
|
@@ -9,11 +9,12 @@ from collections.abc import AsyncGenerator, AsyncIterator
|
|
|
9
9
|
from contextlib import asynccontextmanager
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from types import TracebackType
|
|
12
|
-
from typing import Any, Literal, Self, cast
|
|
12
|
+
from typing import Annotated, Any, Literal, Self, cast
|
|
13
13
|
|
|
14
14
|
import anyio
|
|
15
15
|
import httpx2
|
|
16
16
|
from mcp import ClientSession, types
|
|
17
|
+
from mcp.client._probe import negotiate_auto
|
|
17
18
|
from mcp.client.streamable_http import RequestContext, StreamableHTTPTransport
|
|
18
19
|
from mcp.shared._context_streams import (
|
|
19
20
|
ContextReceiveStream,
|
|
@@ -21,6 +22,7 @@ from mcp.shared._context_streams import (
|
|
|
21
22
|
create_context_streams,
|
|
22
23
|
)
|
|
23
24
|
from mcp.shared.message import SessionMessage
|
|
25
|
+
from pydantic import BaseModel, Field, model_validator
|
|
24
26
|
from structlog.contextvars import bound_contextvars, get_contextvars
|
|
25
27
|
|
|
26
28
|
from ms_tau_sdk.settings import TauSDKSettings
|
|
@@ -218,6 +220,73 @@ def _validate_response_timeout(seconds: float) -> None:
|
|
|
218
220
|
raise ValueError("MCP response timeout must be finite and greater than zero")
|
|
219
221
|
|
|
220
222
|
|
|
223
|
+
SKILLS_EXTENSION = "io.modelcontextprotocol/skills"
|
|
224
|
+
"""The MCP Skills extension (SEP-2640). A server declares it only on protocol 2026-07-28."""
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class SkillFile(BaseModel):
|
|
228
|
+
"""One file of a published skill: its URI, and the size and SHA-256 digest of its bytes."""
|
|
229
|
+
|
|
230
|
+
uri: str
|
|
231
|
+
digest: Annotated[str, Field(pattern=r"^sha256:[0-9a-f]{64}$")]
|
|
232
|
+
size: Annotated[int, Field(ge=0)]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class Skill(BaseModel):
|
|
236
|
+
"""One skill a server publishes: the URI of its ``SKILL.md``, its frontmatter and its files.
|
|
237
|
+
|
|
238
|
+
``resources`` lists every file of the skill, or is ``"dynamic"`` when the server generates the
|
|
239
|
+
skill's content and publishes no digests.
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
uri: Annotated[str, Field(pattern=r"/SKILL\.md$")]
|
|
243
|
+
frontmatter: dict[str, Any]
|
|
244
|
+
resources: list[SkillFile] | Literal["dynamic"]
|
|
245
|
+
|
|
246
|
+
@model_validator(mode="after")
|
|
247
|
+
def _named_and_described(self) -> Self:
|
|
248
|
+
for field in ("name", "description"):
|
|
249
|
+
if not isinstance(self.frontmatter.get(field), str):
|
|
250
|
+
raise ValueError(f"the frontmatter of skill {self.uri} has no {field}")
|
|
251
|
+
return self
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class _ListSkillsRequest(types.PaginatedRequest[Literal["skills/list"]]):
|
|
255
|
+
method: Literal["skills/list"] = "skills/list"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class _ListSkillsResult(types.PaginatedResult, types.CacheableResult):
|
|
259
|
+
skills: list[Skill]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def _list_skills(session: ClientSession) -> tuple[Skill, ...]:
|
|
263
|
+
"""Read every page of ``skills/list``; a skill listed twice counts once."""
|
|
264
|
+
|
|
265
|
+
skills: dict[str, Skill] = {}
|
|
266
|
+
cursor: str | None = None
|
|
267
|
+
cursors: set[str] = set()
|
|
268
|
+
while True:
|
|
269
|
+
page = await session.send_request(
|
|
270
|
+
_ListSkillsRequest(
|
|
271
|
+
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None
|
|
272
|
+
),
|
|
273
|
+
_ListSkillsResult,
|
|
274
|
+
)
|
|
275
|
+
for skill in page.skills:
|
|
276
|
+
skills.setdefault(skill.uri, skill)
|
|
277
|
+
cursor = page.next_cursor
|
|
278
|
+
if cursor is None:
|
|
279
|
+
return tuple(skills.values())
|
|
280
|
+
if cursor in cursors:
|
|
281
|
+
raise ValueError("the server repeated a skills/list cursor")
|
|
282
|
+
cursors.add(cursor)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _sends(request: httpx2.Request, method: str) -> bool:
|
|
286
|
+
message = types.jsonrpc_message_adapter.validate_json(request.content)
|
|
287
|
+
return isinstance(message, types.JSONRPCRequest) and message.method == method
|
|
288
|
+
|
|
289
|
+
|
|
221
290
|
@dataclass(frozen=True)
|
|
222
291
|
class _CallToolCommand:
|
|
223
292
|
name: str
|
|
@@ -235,12 +304,18 @@ class _ReadResourceCommand:
|
|
|
235
304
|
result: asyncio.Future[types.ReadResourceResult]
|
|
236
305
|
|
|
237
306
|
|
|
307
|
+
@dataclass(frozen=True)
|
|
308
|
+
class _ListSkillsCommand:
|
|
309
|
+
log_context: dict[str, object]
|
|
310
|
+
result: asyncio.Future[tuple[Skill, ...]]
|
|
311
|
+
|
|
312
|
+
|
|
238
313
|
@dataclass(frozen=True)
|
|
239
314
|
class _CloseCommand:
|
|
240
315
|
reason: Literal["client_close"] = "client_close"
|
|
241
316
|
|
|
242
317
|
|
|
243
|
-
type _MCPCommand = _CallToolCommand | _ReadResourceCommand | _CloseCommand
|
|
318
|
+
type _MCPCommand = _CallToolCommand | _ReadResourceCommand | _ListSkillsCommand | _CloseCommand
|
|
244
319
|
|
|
245
320
|
ENVIRONMENT_SCOPED_AGENT_TOOLS = frozenset({"agent.list", "agent.search"})
|
|
246
321
|
ENVIRONMENT_UID_ARGUMENT = "organization_environment_uid"
|
|
@@ -258,7 +333,9 @@ class MCPConnectionClient:
|
|
|
258
333
|
"""One remote MCP server over Streamable HTTP.
|
|
259
334
|
|
|
260
335
|
Each client owns its own MCP session, tool catalog and resources; nothing is shared between
|
|
261
|
-
connections.
|
|
336
|
+
connections. With ``mode="legacy"`` the session opens with the ``initialize`` handshake. With
|
|
337
|
+
``mode="auto"``, MCP's own default, it first asks protocol 2026-07-28's ``server/discover``,
|
|
338
|
+
where a server declares its extensions, and opens an older server with ``initialize``.
|
|
262
339
|
"""
|
|
263
340
|
|
|
264
341
|
def __init__(
|
|
@@ -271,6 +348,7 @@ class MCPConnectionClient:
|
|
|
271
348
|
timeout: httpx2.Timeout,
|
|
272
349
|
read_timeout_seconds: float,
|
|
273
350
|
read_concurrency: int,
|
|
351
|
+
mode: Literal["legacy", "auto"] = "legacy",
|
|
274
352
|
) -> None:
|
|
275
353
|
_validate_response_timeout(read_timeout_seconds)
|
|
276
354
|
self.name = name
|
|
@@ -280,6 +358,7 @@ class MCPConnectionClient:
|
|
|
280
358
|
self._http_timeout = timeout
|
|
281
359
|
self._read_timeout_seconds = read_timeout_seconds
|
|
282
360
|
self._read_concurrency = read_concurrency
|
|
361
|
+
self._mode = mode
|
|
283
362
|
self._commands: asyncio.Queue[_MCPCommand] | None = None
|
|
284
363
|
self._owner_task: asyncio.Task[None] | None = None
|
|
285
364
|
self._ready: asyncio.Future[None] | None = None
|
|
@@ -290,6 +369,7 @@ class MCPConnectionClient:
|
|
|
290
369
|
self._http_calls: dict[types.RequestId, _ToolCall] = {}
|
|
291
370
|
self.tools: tuple[types.Tool, ...] = ()
|
|
292
371
|
self.resources: tuple[types.Resource, ...] = ()
|
|
372
|
+
self.extensions: dict[str, dict[str, Any]] = {}
|
|
293
373
|
self._parallel_tool_names: frozenset[str] = frozenset()
|
|
294
374
|
self._closed = False
|
|
295
375
|
|
|
@@ -346,8 +426,7 @@ class MCPConnectionClient:
|
|
|
346
426
|
read_timeout_seconds=self._read_timeout_seconds,
|
|
347
427
|
) as session:
|
|
348
428
|
with bound_contextvars(**initial_log_context):
|
|
349
|
-
|
|
350
|
-
await self._read_server_catalog(session, initialized)
|
|
429
|
+
await self._read_server_catalog(session, await self._negotiate(session))
|
|
351
430
|
self._parallel_tool_names = frozenset(
|
|
352
431
|
tool.name
|
|
353
432
|
for tool in self.tools
|
|
@@ -391,16 +470,27 @@ class MCPConnectionClient:
|
|
|
391
470
|
# MCP 2.x answers an HTTP error on a message POST with a JSON-RPC error that drops the
|
|
392
471
|
# status. Raise it, as MCP 1.x did, so a failure still says refused, server error or
|
|
393
472
|
# timeout. 401 is left to the credential's renewal, and 404 to MCP, as 1.x did.
|
|
394
|
-
if response.request.method
|
|
395
|
-
|
|
473
|
+
if response.request.method != "POST" or response.status_code in {401, 404}:
|
|
474
|
+
return
|
|
475
|
+
# An older server can refuse the `server/discover` probe with a client error. MCP then
|
|
476
|
+
# opens the session with `initialize` instead.
|
|
477
|
+
if response.is_client_error and _sends(response.request, "server/discover"):
|
|
478
|
+
return
|
|
479
|
+
response.raise_for_status()
|
|
480
|
+
|
|
481
|
+
async def _negotiate(self, session: ClientSession) -> types.ServerCapabilities | None:
|
|
482
|
+
if self._mode == "auto":
|
|
483
|
+
await negotiate_auto(session)
|
|
484
|
+
return session.server_capabilities
|
|
485
|
+
initialized = await session.initialize()
|
|
486
|
+
return initialized.capabilities if initialized is not None else None
|
|
396
487
|
|
|
397
488
|
async def _read_server_catalog(
|
|
398
489
|
self,
|
|
399
490
|
session: ClientSession,
|
|
400
|
-
|
|
491
|
+
capabilities: types.ServerCapabilities | None,
|
|
401
492
|
) -> None:
|
|
402
493
|
# Read only what the server offers; a server without resources answers "not found".
|
|
403
|
-
capabilities = initialized.capabilities if initialized is not None else None
|
|
404
494
|
offers_tools = capabilities is None or capabilities.tools is not None
|
|
405
495
|
offers_resources = capabilities is None or capabilities.resources is not None
|
|
406
496
|
tools_result, resources_result = await asyncio.gather(
|
|
@@ -409,6 +499,7 @@ class MCPConnectionClient:
|
|
|
409
499
|
)
|
|
410
500
|
self.tools = tuple(tools_result.tools)
|
|
411
501
|
self.resources = tuple(resources_result.resources)
|
|
502
|
+
self.extensions = dict(capabilities.extensions or {}) if capabilities is not None else {}
|
|
412
503
|
|
|
413
504
|
async def _serve(self, session: ClientSession) -> None:
|
|
414
505
|
commands = self._commands
|
|
@@ -417,7 +508,9 @@ class MCPConnectionClient:
|
|
|
417
508
|
active_reads: set[asyncio.Task[None]] = set()
|
|
418
509
|
read_slots = asyncio.Semaphore(self._read_concurrency)
|
|
419
510
|
|
|
420
|
-
async def execute(
|
|
511
|
+
async def execute(
|
|
512
|
+
command: _CallToolCommand | _ReadResourceCommand | _ListSkillsCommand,
|
|
513
|
+
) -> None:
|
|
421
514
|
if command.result.cancelled():
|
|
422
515
|
return
|
|
423
516
|
self._in_flight.add(command.result)
|
|
@@ -450,10 +543,14 @@ class MCPConnectionClient:
|
|
|
450
543
|
command.result.remove_done_callback(call.cancel_if_abandoned)
|
|
451
544
|
_tool_call.reset(token)
|
|
452
545
|
call.finish()
|
|
453
|
-
|
|
546
|
+
elif isinstance(command, _ReadResourceCommand):
|
|
454
547
|
resource_result = await session.read_resource(command.uri)
|
|
455
548
|
if not command.result.done():
|
|
456
549
|
command.result.set_result(resource_result)
|
|
550
|
+
else:
|
|
551
|
+
skills = await _list_skills(session)
|
|
552
|
+
if not command.result.done():
|
|
553
|
+
command.result.set_result(skills)
|
|
457
554
|
except Exception as error:
|
|
458
555
|
if not command.result.done():
|
|
459
556
|
command.result.set_exception(error)
|
|
@@ -464,7 +561,7 @@ class MCPConnectionClient:
|
|
|
464
561
|
self._in_flight.discard(command.result)
|
|
465
562
|
|
|
466
563
|
async def execute_read(
|
|
467
|
-
command: _CallToolCommand | _ReadResourceCommand,
|
|
564
|
+
command: _CallToolCommand | _ReadResourceCommand | _ListSkillsCommand,
|
|
468
565
|
) -> None:
|
|
469
566
|
async with read_slots:
|
|
470
567
|
await execute(command)
|
|
@@ -479,7 +576,9 @@ class MCPConnectionClient:
|
|
|
479
576
|
if isinstance(command, _CloseCommand):
|
|
480
577
|
await drain_reads()
|
|
481
578
|
return
|
|
482
|
-
is_parallel_read = isinstance(
|
|
579
|
+
is_parallel_read = isinstance(
|
|
580
|
+
command, _ReadResourceCommand | _ListSkillsCommand
|
|
581
|
+
) or (
|
|
483
582
|
isinstance(command, _CallToolCommand)
|
|
484
583
|
and command.name in self._parallel_tool_names
|
|
485
584
|
)
|
|
@@ -563,6 +662,19 @@ class MCPConnectionClient:
|
|
|
563
662
|
)
|
|
564
663
|
return await result
|
|
565
664
|
|
|
665
|
+
async def list_skills(self) -> tuple[Skill, ...]:
|
|
666
|
+
"""Every skill the server publishes; none when it does not declare ``SKILLS_EXTENSION``.
|
|
667
|
+
|
|
668
|
+
Only a server that declares the extension is asked, as the extension requires.
|
|
669
|
+
"""
|
|
670
|
+
|
|
671
|
+
if SKILLS_EXTENSION not in self.extensions:
|
|
672
|
+
return ()
|
|
673
|
+
commands = self._require_commands()
|
|
674
|
+
result: asyncio.Future[tuple[Skill, ...]] = asyncio.get_running_loop().create_future()
|
|
675
|
+
commands.put_nowait(_ListSkillsCommand(log_context=dict(get_contextvars()), result=result))
|
|
676
|
+
return await result
|
|
677
|
+
|
|
566
678
|
async def aclose(self) -> None:
|
|
567
679
|
if self._closed:
|
|
568
680
|
return
|
{ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/tools/mcp_applications.py
RENAMED
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
"""MCP endpoints of the applications an Agent declares (ADR 0021, section 4).
|
|
1
|
+
"""MCP endpoints of the applications an Agent declares (ADR 0021, section 4; ADR 0023).
|
|
2
2
|
|
|
3
3
|
Declaring an application registers its MCP; it grants no access. The platform resolves each
|
|
4
4
|
declared application in the Agent's Environment and hands the runtime its name and release at
|
|
5
|
-
startup. Each application gets
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
startup. Each application gets four tools: ``<name>__list_tools`` and ``<name>__call_tool`` for its
|
|
6
|
+
tools, and ``<name>__list_resources`` and ``<name>__read_resource`` for the skills and resources it
|
|
7
|
+
publishes. They run inside the turn: the SDK obtains the application's address and a short-lived
|
|
8
|
+
token, opens an MCP session for the call, and closes it afterwards. The token carries the
|
|
9
|
+
delegation of the person the turn serves, when it serves one, and is the Agent's own otherwise; the
|
|
10
|
+
application decides what the call may do. No session, token or catalog is shared between turns or
|
|
11
|
+
people, and nothing is read when the session loads, because no turn is running then.
|
|
11
12
|
|
|
12
13
|
Each tool declares how long it may run in its ``_meta`` (ADR 0022); a tool that declares nothing
|
|
13
14
|
gets ``TAU_MCP_TOOL_TIMEOUT_SECONDS``. A failure reaches the model in plain words, and
|
|
14
15
|
opening the session is tried once more when it fails before the tool call is sent.
|
|
16
|
+
|
|
17
|
+
A file of a skill the application lists (MCP Skills extension) reaches the model only when its
|
|
18
|
+
bytes match the size and SHA-256 digest the listing gives. Published skills are the application's
|
|
19
|
+
content: the SDK never adds them to the project's skills and grants nothing they ask for.
|
|
15
20
|
"""
|
|
16
21
|
|
|
17
22
|
from __future__ import annotations
|
|
18
23
|
|
|
24
|
+
import base64
|
|
25
|
+
import binascii
|
|
26
|
+
import hashlib
|
|
19
27
|
import json
|
|
20
28
|
import re
|
|
21
29
|
from collections.abc import AsyncGenerator, Mapping, Sequence
|
|
@@ -34,7 +42,13 @@ from tau_agent.tools import (
|
|
|
34
42
|
)
|
|
35
43
|
from tau_agent.types import JSONValue
|
|
36
44
|
|
|
37
|
-
from ms_tau_sdk.backend.mcp import
|
|
45
|
+
from ms_tau_sdk.backend.mcp import (
|
|
46
|
+
SKILLS_EXTENSION,
|
|
47
|
+
MCPConnectionClient,
|
|
48
|
+
Skill,
|
|
49
|
+
SkillFile,
|
|
50
|
+
raise_for_renewed_401,
|
|
51
|
+
)
|
|
38
52
|
from ms_tau_sdk.backend.models import MCPApplication
|
|
39
53
|
from ms_tau_sdk.errors import (
|
|
40
54
|
ApplicationReadinessTimeoutError,
|
|
@@ -109,6 +123,8 @@ def application_connector(settings: TauSDKSettings) -> ApplicationConnector:
|
|
|
109
123
|
),
|
|
110
124
|
read_timeout_seconds=settings.backend_read_timeout_seconds,
|
|
111
125
|
read_concurrency=settings.mcp_read_concurrency,
|
|
126
|
+
# An application declares the Skills extension only on the newer protocol revision.
|
|
127
|
+
mode="auto",
|
|
112
128
|
).open()
|
|
113
129
|
|
|
114
130
|
return connect
|
|
@@ -222,6 +238,71 @@ def _tool_entry(tool: types.Tool) -> dict[str, JSONValue]:
|
|
|
222
238
|
return entry
|
|
223
239
|
|
|
224
240
|
|
|
241
|
+
def _owner(skills: Sequence[Skill], uri: str) -> Skill | None:
|
|
242
|
+
"""The listed skill whose directory holds ``uri``; the innermost one when skills nest."""
|
|
243
|
+
|
|
244
|
+
owners = [skill for skill in skills if uri.startswith(skill.uri.removesuffix("SKILL.md"))]
|
|
245
|
+
return max(owners, key=lambda skill: len(skill.uri), default=None)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _listed_by_skill(skills: Sequence[Skill], uri: str) -> bool:
|
|
249
|
+
"""Whether ``uri`` is read as a file of a listed skill rather than as a plain resource.
|
|
250
|
+
|
|
251
|
+
A skill with a file list owns its whole directory. A generated (``"dynamic"``) skill lists no
|
|
252
|
+
files, so only its ``SKILL.md`` is its own.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
owner = _owner(skills, uri)
|
|
256
|
+
return owner is not None and (owner.resources != "dynamic" or uri == owner.uri)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _skill_entry(skill: Skill) -> dict[str, JSONValue]:
|
|
260
|
+
entry: dict[str, JSONValue] = {
|
|
261
|
+
"uri": skill.uri,
|
|
262
|
+
"name": skill.frontmatter["name"],
|
|
263
|
+
"description": skill.frontmatter["description"],
|
|
264
|
+
}
|
|
265
|
+
if skill.resources != "dynamic":
|
|
266
|
+
entry["files"] = [{"uri": file.uri, "size": file.size} for file in skill.resources]
|
|
267
|
+
return entry
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _resource_entry(resource: types.Resource) -> dict[str, JSONValue]:
|
|
271
|
+
entry: dict[str, JSONValue] = {"uri": str(resource.uri), "name": resource.name}
|
|
272
|
+
if resource.description:
|
|
273
|
+
entry["description"] = resource.description
|
|
274
|
+
if resource.mime_type:
|
|
275
|
+
entry["mimeType"] = resource.mime_type
|
|
276
|
+
return entry
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _matches(
|
|
280
|
+
contents: Sequence[types.TextResourceContents | types.BlobResourceContents],
|
|
281
|
+
listed: SkillFile,
|
|
282
|
+
) -> bool:
|
|
283
|
+
"""Whether a read returned exactly one content, with the bytes the skill lists."""
|
|
284
|
+
|
|
285
|
+
if len(contents) != 1:
|
|
286
|
+
return False
|
|
287
|
+
content = contents[0]
|
|
288
|
+
if isinstance(content, types.TextResourceContents):
|
|
289
|
+
data = content.text.encode()
|
|
290
|
+
else:
|
|
291
|
+
try:
|
|
292
|
+
data = base64.b64decode(content.blob, validate=True)
|
|
293
|
+
except binascii.Error:
|
|
294
|
+
return False
|
|
295
|
+
return (
|
|
296
|
+
len(data) == listed.size and f"sha256:{hashlib.sha256(data).hexdigest()}" == listed.digest
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _content_text(content: types.TextResourceContents | types.BlobResourceContents) -> str:
|
|
301
|
+
if isinstance(content, types.TextResourceContents):
|
|
302
|
+
return content.text
|
|
303
|
+
return f"(Binary content of type {content.mime_type or 'unknown'}; not shown.)"
|
|
304
|
+
|
|
305
|
+
|
|
225
306
|
def _application_tools(
|
|
226
307
|
application: MCPApplication,
|
|
227
308
|
*,
|
|
@@ -286,21 +367,158 @@ def _application_tools(
|
|
|
286
367
|
client = await open_session(signal)
|
|
287
368
|
try:
|
|
288
369
|
tools = client.tools
|
|
370
|
+
publishes_skills = SKILLS_EXTENSION in client.extensions
|
|
371
|
+
publishes_resources = bool(client.resources)
|
|
289
372
|
finally:
|
|
290
373
|
await client.aclose()
|
|
291
374
|
except Exception as error:
|
|
292
375
|
return failed("list_tools", error)
|
|
293
376
|
entries = [_tool_entry(tool) for tool in tools]
|
|
377
|
+
published = ""
|
|
378
|
+
if publishes_skills:
|
|
379
|
+
published = (
|
|
380
|
+
" It also publishes skills, instructions for using its tools: list them with "
|
|
381
|
+
f"{name}__list_resources."
|
|
382
|
+
)
|
|
383
|
+
elif publishes_resources:
|
|
384
|
+
published = f" It also publishes resources: list them with {name}__list_resources."
|
|
294
385
|
return AgentToolResult(
|
|
295
386
|
content=[
|
|
296
387
|
TextContent(
|
|
297
|
-
text=f"Tools of the {name} application. Call one with {name}__call_tool
|
|
388
|
+
text=f"Tools of the {name} application. Call one with {name}__call_tool."
|
|
389
|
+
+ published
|
|
390
|
+
+ "\n"
|
|
298
391
|
+ json.dumps(entries, indent=2, sort_keys=True, default=str)
|
|
299
392
|
)
|
|
300
393
|
],
|
|
301
394
|
details={"application": name, "is_error": False, "tools": [t.name for t in tools]},
|
|
302
395
|
)
|
|
303
396
|
|
|
397
|
+
async def list_resources(
|
|
398
|
+
tool_call_id: str,
|
|
399
|
+
arguments: Mapping[str, JSONValue],
|
|
400
|
+
signal: ToolCancellationToken | None = None,
|
|
401
|
+
on_update: ToolUpdateCallback | None = None,
|
|
402
|
+
) -> AgentToolResult:
|
|
403
|
+
del tool_call_id, arguments, on_update
|
|
404
|
+
if signal is not None and signal.is_cancelled():
|
|
405
|
+
return _error(name, f"Listing the {name} resources was cancelled.", cancelled=True)
|
|
406
|
+
try:
|
|
407
|
+
client = await open_session(signal)
|
|
408
|
+
try:
|
|
409
|
+
skills = await client.list_skills()
|
|
410
|
+
resources = client.resources
|
|
411
|
+
finally:
|
|
412
|
+
await client.aclose()
|
|
413
|
+
except Exception as error:
|
|
414
|
+
return failed("list_resources", error)
|
|
415
|
+
listing: dict[str, list[dict[str, JSONValue]]] = {
|
|
416
|
+
"skills": [_skill_entry(skill) for skill in skills],
|
|
417
|
+
"resources": [
|
|
418
|
+
_resource_entry(resource)
|
|
419
|
+
for resource in resources
|
|
420
|
+
if not _listed_by_skill(skills, str(resource.uri))
|
|
421
|
+
],
|
|
422
|
+
}
|
|
423
|
+
if listing["skills"] or listing["resources"]:
|
|
424
|
+
text = (
|
|
425
|
+
f"Skills and resources the {name} application publishes. A skill is the "
|
|
426
|
+
f"application's instructions for using its tools. Read a skill file or a resource "
|
|
427
|
+
f"with {name}__read_resource.\n"
|
|
428
|
+
+ json.dumps(listing, indent=2, sort_keys=True, default=str)
|
|
429
|
+
)
|
|
430
|
+
else:
|
|
431
|
+
text = f"The {name} application publishes no skills or resources."
|
|
432
|
+
return AgentToolResult(
|
|
433
|
+
content=[TextContent(text=text)],
|
|
434
|
+
details={
|
|
435
|
+
"application": name,
|
|
436
|
+
"is_error": False,
|
|
437
|
+
"skills": [skill.uri for skill in skills],
|
|
438
|
+
"resources": [entry["uri"] for entry in listing["resources"]],
|
|
439
|
+
},
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
async def read_resource(
|
|
443
|
+
tool_call_id: str,
|
|
444
|
+
arguments: Mapping[str, JSONValue],
|
|
445
|
+
signal: ToolCancellationToken | None = None,
|
|
446
|
+
on_update: ToolUpdateCallback | None = None,
|
|
447
|
+
) -> AgentToolResult:
|
|
448
|
+
del tool_call_id, on_update
|
|
449
|
+
uri = arguments.get("uri")
|
|
450
|
+
if not isinstance(uri, str) or not uri:
|
|
451
|
+
return _error(name, f"Name the {name} skill file or resource to read in `uri`.")
|
|
452
|
+
if signal is not None and signal.is_cancelled():
|
|
453
|
+
return _error(name, f"Reading the {name} resource was cancelled.", cancelled=True)
|
|
454
|
+
result: types.ReadResourceResult | None = None
|
|
455
|
+
owner: Skill | None = None
|
|
456
|
+
listed: SkillFile | None = None
|
|
457
|
+
try:
|
|
458
|
+
client = await open_session(signal)
|
|
459
|
+
try:
|
|
460
|
+
# Only what the application lists is read: a file of a listed skill, which must
|
|
461
|
+
# match the skill's list, or an advertised resource.
|
|
462
|
+
owner = _owner(await client.list_skills(), uri)
|
|
463
|
+
if owner is not None and owner.resources != "dynamic":
|
|
464
|
+
listed = next((file for file in owner.resources if file.uri == uri), None)
|
|
465
|
+
readable = listed is not None
|
|
466
|
+
else:
|
|
467
|
+
readable = (owner is not None and uri == owner.uri) or any(
|
|
468
|
+
str(resource.uri) == uri for resource in client.resources
|
|
469
|
+
)
|
|
470
|
+
if readable:
|
|
471
|
+
result = await client.read_resource(uri)
|
|
472
|
+
finally:
|
|
473
|
+
await client.aclose()
|
|
474
|
+
except Exception as error:
|
|
475
|
+
return failed("read_resource", error)
|
|
476
|
+
if result is None:
|
|
477
|
+
if owner is not None and owner.resources != "dynamic":
|
|
478
|
+
return _error(
|
|
479
|
+
name,
|
|
480
|
+
f"{uri} is not a file of the {name} skill {owner.frontmatter['name']} as the "
|
|
481
|
+
f"application lists it. List its files with {name}__list_resources.",
|
|
482
|
+
uri=uri,
|
|
483
|
+
failure="not_listed",
|
|
484
|
+
)
|
|
485
|
+
return _error(
|
|
486
|
+
name,
|
|
487
|
+
f"The {name} application does not publish {uri}. List what it publishes with "
|
|
488
|
+
f"{name}__list_resources.",
|
|
489
|
+
uri=uri,
|
|
490
|
+
failure="not_published",
|
|
491
|
+
)
|
|
492
|
+
contents = result.contents
|
|
493
|
+
if listed is not None:
|
|
494
|
+
contents = [content for content in contents if str(content.uri) == uri]
|
|
495
|
+
if not _matches(contents, listed):
|
|
496
|
+
logger.warning(
|
|
497
|
+
"runtime.mcp_application.failed",
|
|
498
|
+
application=name,
|
|
499
|
+
operation="read_resource",
|
|
500
|
+
failure="unverified",
|
|
501
|
+
)
|
|
502
|
+
return _error(
|
|
503
|
+
name,
|
|
504
|
+
f"The {name} application served {uri} with other content than its skill "
|
|
505
|
+
f"lists, so it is not shown. List the skill again with {name}__list_resources.",
|
|
506
|
+
uri=uri,
|
|
507
|
+
failure="unverified",
|
|
508
|
+
)
|
|
509
|
+
origin = f"From the {name} application: {uri}"
|
|
510
|
+
details: dict[str, JSONValue] = {"application": name, "is_error": False, "uri": uri}
|
|
511
|
+
if owner is not None:
|
|
512
|
+
origin += f", a file of its skill {owner.frontmatter['name']}"
|
|
513
|
+
details |= {"skill": owner.uri, "verified": listed is not None}
|
|
514
|
+
return AgentToolResult(
|
|
515
|
+
content=[
|
|
516
|
+
TextContent(text=f"{origin}.\n\n"),
|
|
517
|
+
*(TextContent(text=_content_text(content)) for content in contents),
|
|
518
|
+
],
|
|
519
|
+
details=details,
|
|
520
|
+
)
|
|
521
|
+
|
|
304
522
|
async def call_tool(
|
|
305
523
|
tool_call_id: str,
|
|
306
524
|
arguments: Mapping[str, JSONValue],
|
|
@@ -381,6 +599,38 @@ def _application_tools(
|
|
|
381
599
|
execute_fn=call_tool,
|
|
382
600
|
execution_mode="sequential",
|
|
383
601
|
),
|
|
602
|
+
AgentTool(
|
|
603
|
+
name=f"{name}__list_resources",
|
|
604
|
+
label=f"List {name} skills and resources",
|
|
605
|
+
description=(
|
|
606
|
+
f"List the skills and resources the {name} application publishes. A skill is the "
|
|
607
|
+
"application's instructions for using its tools; each lists its files."
|
|
608
|
+
),
|
|
609
|
+
parameters={"type": "object", "properties": {}, "additionalProperties": False},
|
|
610
|
+
execute_fn=list_resources,
|
|
611
|
+
execution_mode="parallel",
|
|
612
|
+
),
|
|
613
|
+
AgentTool(
|
|
614
|
+
name=f"{name}__read_resource",
|
|
615
|
+
label=f"Read a {name} skill file or resource",
|
|
616
|
+
description=(
|
|
617
|
+
f"Read one skill file or resource the {name} application publishes, by its URI. "
|
|
618
|
+
f"Use {name}__list_resources first to see what it publishes."
|
|
619
|
+
),
|
|
620
|
+
parameters={
|
|
621
|
+
"type": "object",
|
|
622
|
+
"properties": {
|
|
623
|
+
"uri": {
|
|
624
|
+
"type": "string",
|
|
625
|
+
"description": f"The URI of a {name} skill file or resource.",
|
|
626
|
+
},
|
|
627
|
+
},
|
|
628
|
+
"required": ["uri"],
|
|
629
|
+
"additionalProperties": False,
|
|
630
|
+
},
|
|
631
|
+
execute_fn=read_resource,
|
|
632
|
+
execution_mode="parallel",
|
|
633
|
+
),
|
|
384
634
|
]
|
|
385
635
|
|
|
386
636
|
|
|
@@ -391,7 +641,7 @@ def create_mcp_application_tools(
|
|
|
391
641
|
tool_timeout_seconds: float,
|
|
392
642
|
open_timeout_seconds: float,
|
|
393
643
|
) -> list[AgentTool]:
|
|
394
|
-
"""
|
|
644
|
+
"""Four tools for each declared application, in declaration order.
|
|
395
645
|
|
|
396
646
|
``tool_timeout_seconds`` bounds a call to a tool that declares no limit of its own;
|
|
397
647
|
``open_timeout_seconds`` is the wait for each request that opens the session.
|
|
File without changes
|
|
File without changes
|
{ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/__init__.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/app.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/cli.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/config.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/env_file.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/logs.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/proxy.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/packages/tau-board/src/ms_tau_board/state.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/protocols/assistant_ui.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/protocols/chat_history.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/runtime/deployment_health.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ms_tau_sdk-2.0.13.dev55 → ms_tau_sdk-2.0.14.dev57}/src/ms_tau_sdk/tools/mainsequence_mcp.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|