strands-you 0.1.0__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.
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ dist/
3
+ src/strands_you/mcp.json
4
+ src/strands_you/skills/
5
+ __pycache__/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ .mypy_cache/
9
+ *.egg-info/
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.5
2
+ Name: strands-you
3
+ Version: 0.1.0
4
+ Summary: You.com MCP servers and skills for Strands Agents
5
+ Project-URL: Homepage, https://github.com/youdotcom-oss/agent-skills/tree/main/packages/strands-agents/python
6
+ Project-URL: Documentation, https://github.com/youdotcom-oss/agent-skills#readme
7
+ Project-URL: Repository, https://github.com/youdotcom-oss/agent-skills
8
+ Project-URL: Issues, https://github.com/youdotcom-oss/agent-skills/issues
9
+ Author-email: "You.com" <oss@you.com>
10
+ License: MIT
11
+ Keywords: agents,ai,strands,tool
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: pydantic>=2.0.0
22
+ Requires-Dist: strands-agents>=1.0.0
23
+ Description-Content-Type: text/markdown
24
+
25
+ # strands-you
26
+
27
+ You.com MCP servers and Agent Skills for [Strands Agents](https://strandsagents.com), in one Python plugin.
28
+
29
+ `YouDotComPlugin` registers the You.com MCP servers (search, content extraction, cited research, finance data, docs lookup) and the bundled You.com Agent Skills, so a Strands agent gets web-grounded tools and the skills to use them well in one line.
30
+
31
+ ## Install
32
+
33
+ ```sh
34
+ pip install strands-you
35
+ ```
36
+
37
+ ## Quickstart
38
+
39
+ ```python
40
+ from strands import Agent
41
+ from strands_you import YouDotComPlugin
42
+
43
+ agent = Agent(plugins=[YouDotComPlugin()])
44
+ result = agent("What changed in the latest release of strands-agents? Cite sources.")
45
+ ```
46
+
47
+ The API key comes from `YDC_API_KEY` by default, or pass it explicitly: `YouDotComPlugin(api_key="...")`. Get one at [you.com/platform](https://you.com/platform).
48
+
49
+ ## What it configures
50
+
51
+ - MCP servers from `mcp.json`: `you`, `you-finance`, `you-research` (auth via
52
+ `YDC_API_KEY`), plus keyless `you-discover` and `you-docs`
53
+ - Skills from the bundled `skills/`: `you-web`, `you-research`, `you-finance`,
54
+ `you-discover`
55
+
56
+ Without an API key the keyed servers are skipped with a warning and the agent is told which servers are unavailable; keyless servers always work. The shipped server and skill sets mirror the repo-root `skills/` and `mcp.json` at release time.
57
+
58
+ ## Build and test
59
+
60
+ ```sh
61
+ bun scripts/stage.ts # stage shared skills + mcp.json (gitignored)
62
+ uv sync --group dev
63
+ uv run pytest -q
64
+ ```
65
+
66
+ The Python package shares its version line with the TypeScript package (`packages/strands-agents/typescript`); both are stamped by the release plan's `strands` unit.
67
+
68
+ ## License
69
+
70
+ MIT. Bundled skills are from this repository, MIT.
@@ -0,0 +1,46 @@
1
+ # strands-you
2
+
3
+ You.com MCP servers and Agent Skills for [Strands Agents](https://strandsagents.com), in one Python plugin.
4
+
5
+ `YouDotComPlugin` registers the You.com MCP servers (search, content extraction, cited research, finance data, docs lookup) and the bundled You.com Agent Skills, so a Strands agent gets web-grounded tools and the skills to use them well in one line.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pip install strands-you
11
+ ```
12
+
13
+ ## Quickstart
14
+
15
+ ```python
16
+ from strands import Agent
17
+ from strands_you import YouDotComPlugin
18
+
19
+ agent = Agent(plugins=[YouDotComPlugin()])
20
+ result = agent("What changed in the latest release of strands-agents? Cite sources.")
21
+ ```
22
+
23
+ The API key comes from `YDC_API_KEY` by default, or pass it explicitly: `YouDotComPlugin(api_key="...")`. Get one at [you.com/platform](https://you.com/platform).
24
+
25
+ ## What it configures
26
+
27
+ - MCP servers from `mcp.json`: `you`, `you-finance`, `you-research` (auth via
28
+ `YDC_API_KEY`), plus keyless `you-discover` and `you-docs`
29
+ - Skills from the bundled `skills/`: `you-web`, `you-research`, `you-finance`,
30
+ `you-discover`
31
+
32
+ Without an API key the keyed servers are skipped with a warning and the agent is told which servers are unavailable; keyless servers always work. The shipped server and skill sets mirror the repo-root `skills/` and `mcp.json` at release time.
33
+
34
+ ## Build and test
35
+
36
+ ```sh
37
+ bun scripts/stage.ts # stage shared skills + mcp.json (gitignored)
38
+ uv sync --group dev
39
+ uv run pytest -q
40
+ ```
41
+
42
+ The Python package shares its version line with the TypeScript package (`packages/strands-agents/typescript`); both are stamped by the release plan's `strands` unit.
43
+
44
+ ## License
45
+
46
+ MIT. Bundled skills are from this repository, MIT.
@@ -0,0 +1,74 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "strands-you"
7
+ version = "0.1.0"
8
+ description = "You.com MCP servers and skills for Strands Agents"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "You.com", email = "oss@you.com"}
14
+ ]
15
+ keywords = ["strands", "agents", "ai", "tool"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ ]
26
+
27
+ dependencies = [
28
+ "strands-agents>=1.0.0",
29
+ "pydantic>=2.0.0",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/youdotcom-oss/agent-skills/tree/main/packages/strands-agents/python"
34
+ Documentation = "https://github.com/youdotcom-oss/agent-skills#readme"
35
+ Repository = "https://github.com/youdotcom-oss/agent-skills"
36
+ Issues = "https://github.com/youdotcom-oss/agent-skills/issues"
37
+
38
+ [dependency-groups]
39
+ dev = [
40
+ "pytest>=8.0.0,<9.0.0",
41
+ "pytest-asyncio>=0.25.0,<1.0.0",
42
+ "ruff>=0.11.0,<1.0.0",
43
+ "mypy>=1.15.0,<2.0.0",
44
+ ]
45
+
46
+ # Environment management is uv: `uv sync` creates .venv from uv.lock, and
47
+ # everything runs through `uv run` (see CI). Hatchling remains the build
48
+ # backend — `uv build` drives it for release artifacts.
49
+
50
+ [tool.hatch.build.targets.wheel]
51
+ packages = ["src/strands_you"]
52
+
53
+ [tool.ruff]
54
+ line-length = 120
55
+ include = ["src/**/*.py", "tests/**/*.py"]
56
+
57
+ [tool.ruff.lint]
58
+ select = [
59
+ "E", # pycodestyle
60
+ "F", # pyflakes
61
+ "I", # isort
62
+ "B", # flake8-bugbear
63
+ ]
64
+
65
+ [tool.mypy]
66
+ python_version = "3.10"
67
+ warn_return_any = true
68
+ warn_unused_configs = true
69
+ ignore_missing_imports = true
70
+
71
+ [tool.pytest.ini_options]
72
+ testpaths = ["tests"]
73
+ pythonpath = ["src"]
74
+ asyncio_mode = "auto"
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Stage the shared You.com assets from the repo root into the Python package
3
+ * (gitignored build inputs): repo-root skills/ + mcp.json ->
4
+ * src/strands_you/{skills/,mcp.json}. Run before `uv build` and
5
+ * before tests — the plugin loads the shipped assets via importlib.resources,
6
+ * so tests exercise exactly what the wheel will contain.
7
+ *
8
+ * MINIMAL: staging logic duplicated per package instead of a shared root
9
+ * helper — the house pattern is self-contained package builds. Upgrade path:
10
+ * extract a shared scripts/copy-skills.ts when a sixth consumer appears.
11
+ */
12
+ import { cpSync, readdirSync, rmSync, statSync } from 'node:fs'
13
+
14
+ const repoRoot = new URL('../../../../', import.meta.url)
15
+ const packageAssetsDir = new URL('../src/strands_you/', import.meta.url)
16
+
17
+ const isDirectory = (path: URL) => statSync(path, { throwIfNoEntry: false })?.isDirectory()
18
+
19
+ if (!isDirectory(new URL('skills/', repoRoot))) throw new Error(`Missing source skills directory: ${repoRoot}skills/`)
20
+
21
+ rmSync(new URL('skills/', packageAssetsDir), { recursive: true, force: true })
22
+ rmSync(new URL('mcp.json', packageAssetsDir), { force: true })
23
+
24
+ let staged = 0
25
+ for (const entry of readdirSync(new URL('skills/', repoRoot))) {
26
+ const sourceSkillDir = new URL(`skills/${entry}/`, repoRoot)
27
+ if (isDirectory(sourceSkillDir) && statSync(new URL('SKILL.md', sourceSkillDir), { throwIfNoEntry: false })) {
28
+ cpSync(sourceSkillDir, new URL(`skills/${entry}/`, packageAssetsDir), { recursive: true })
29
+ staged += 1
30
+ }
31
+ }
32
+ if (staged === 0) throw new Error(`No skills found in ${repoRoot}skills/`)
33
+ cpSync(new URL('mcp.json', repoRoot), new URL('mcp.json', packageAssetsDir), { force: true })
34
+
35
+ console.log(`staged ${staged} skills + mcp.json -> src/strands_you/`)
@@ -0,0 +1,7 @@
1
+ """Strands Package."""
2
+
3
+ from strands_you.plugin import YouDotComPlugin
4
+
5
+ __all__ = [
6
+ "YouDotComPlugin",
7
+ ]
@@ -0,0 +1,117 @@
1
+ """You.com plugin for Strands Agents.
2
+
3
+ Bundles the You.com MCP servers (from the shipped ``mcp.json``) and the You.com
4
+ Agent Skills (from the bundled ``skills/`` directory) into a single plugin:
5
+
6
+ ```python
7
+ from strands import Agent
8
+ from strands_you import YouDotComPlugin
9
+
10
+ agent = Agent(plugins=[YouDotComPlugin()])
11
+ ```
12
+
13
+ Servers that require an API key are enabled only when ``YDC_API_KEY`` (or the
14
+ explicit ``api_key`` argument) is available; keyless servers are always enabled.
15
+ """
16
+
17
+ import json
18
+ import logging
19
+ import os
20
+ import warnings
21
+ from importlib import resources
22
+ from typing import Any
23
+
24
+ from strands.hooks import BeforeInvocationEvent
25
+ from strands.plugins import hook
26
+ from strands.tools.mcp import MCPClient
27
+ from strands.vended_plugins.skills import AgentSkills
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ #: Servers that authenticate with ``YDC_API_KEY``. Any server in the shipped
32
+ #: ``mcp.json`` not listed here is treated as keyless. Keying this policy by
33
+ #: server name keeps the shipped ``mcp.json`` the single source of truth for
34
+ #: server topology (urls) while auth policy stays a consumer-side concern.
35
+ _API_KEY_SERVERS = frozenset({"you", "you-finance", "you-research"})
36
+
37
+ _API_KEY_ENV_VAR = "YDC_API_KEY"
38
+
39
+ _CAPABILITY_NOTE_MARKER = "unavailable You.com MCP servers"
40
+
41
+
42
+ def _load_mcp_config() -> dict[str, dict[str, Any]]:
43
+ """Load the shipped ``mcp.json`` and return its server entries by name."""
44
+ mcp_config: Any = json.loads(resources.files("strands_you").joinpath("mcp.json").read_text(encoding="utf-8"))
45
+ servers: dict[str, dict[str, Any]] = mcp_config.get("mcpServers", {})
46
+ return servers
47
+
48
+
49
+ class YouDotComPlugin(AgentSkills):
50
+ """Strands plugin bundling You.com MCP servers and Agent Skills.
51
+
52
+ Subclasses the SDK's ``AgentSkills`` plugin so the bundled skills are
53
+ discovered and activated by the agent, and registers one MCP client per
54
+ enabled server when attached to an agent.
55
+
56
+ Args:
57
+ api_key: You.com API key. Defaults to the ``YDC_API_KEY`` environment
58
+ variable. When absent, servers that require a key are skipped with
59
+ a warning and only keyless servers are enabled.
60
+ """
61
+
62
+ name = "youdotcom"
63
+
64
+ def __init__(self, api_key: str | None = None) -> None:
65
+ """Initialize the plugin from the shipped ``mcp.json``."""
66
+ self._api_key = api_key if api_key is not None else os.environ.get(_API_KEY_ENV_VAR)
67
+ self._all_servers = _load_mcp_config()
68
+ self._servers = {name: server for name, server in self._all_servers.items() if self._server_enabled(name)}
69
+ self._skipped_servers = sorted(set(self._all_servers) - set(self._servers))
70
+ super().__init__(skills=[str(resources.files("strands_you").joinpath("skills"))])
71
+
72
+ def _server_enabled(self, name: str) -> bool:
73
+ """Return True when the named server is usable with the current auth."""
74
+ if name not in _API_KEY_SERVERS:
75
+ return True
76
+ if self._api_key:
77
+ return True
78
+ warnings.warn(
79
+ f"Server {name!r} requires an API key; set YDC_API_KEY to enable it. Enabled keyless servers only.",
80
+ stacklevel=2,
81
+ )
82
+ return False
83
+
84
+ @property
85
+ def servers(self) -> dict[str, dict[str, Any]]:
86
+ """Enabled MCP server configs by name, as shipped in ``mcp.json``."""
87
+ return dict(self._servers)
88
+
89
+ def init_agent(self, agent: Any) -> None: # type: ignore[override] # registry supports sync init
90
+ """Register one MCP client per enabled server with the agent.
91
+
92
+ Args:
93
+ agent: The agent instance the plugin is attached to.
94
+ """
95
+ for name, server in self._servers.items():
96
+ headers = {"Authorization": f"Bearer {self._api_key}"} if name in _API_KEY_SERVERS else None
97
+ client = MCPClient(url=server["url"], headers=headers)
98
+ agent.tool_registry.process_tools([client])
99
+ logger.debug("server=<%s> | registered MCP client", name)
100
+
101
+ @hook # type: ignore[call-overload] # same as the SDK's own plugin template
102
+ async def _on_before_invocation_note(self, event: BeforeInvocationEvent) -> None:
103
+ """Tell the agent which servers are unavailable when no API key is set.
104
+
105
+ Without this, an agent running keyless discovers its own capability
106
+ boundary only by failing tool calls. Runs on every invocation; the note
107
+ is appended once (idempotent via a stable marker).
108
+ """
109
+ if self._api_key or not self._skipped_servers:
110
+ return
111
+ note = (
112
+ f"The following You.com MCP servers are {_CAPABILITY_NOTE_MARKER} "
113
+ f"because no API key is configured: {', '.join(self._skipped_servers)}. "
114
+ f"Set {_API_KEY_ENV_VAR} to enable them."
115
+ )
116
+ if hasattr(event.agent, "system_prompt") and _CAPABILITY_NOTE_MARKER not in (event.agent.system_prompt or ""):
117
+ event.agent.system_prompt = (event.agent.system_prompt or "") + "\n\n" + note
File without changes
@@ -0,0 +1,137 @@
1
+ """Tests for YouDotComPlugin."""
2
+
3
+ import asyncio
4
+
5
+ import pytest
6
+ from strands import Agent
7
+ from strands.hooks import BeforeInvocationEvent
8
+
9
+ import strands_you.plugin as plugin_mod
10
+ from strands_you import YouDotComPlugin
11
+
12
+ #: The server set comes from the shipped mcp.json mirror, which the release
13
+ #: sync owns. Expectations are derived from it so a sync that adds or removes
14
+ #: a server never breaks these tests — the auth policy, not the topology, is
15
+ #: what they pin down.
16
+ ALL_SERVERS = set(plugin_mod._load_mcp_config())
17
+ KEYED_SERVERS = set(plugin_mod._API_KEY_SERVERS)
18
+ KEYLESS_SERVERS = ALL_SERVERS - KEYED_SERVERS
19
+
20
+
21
+ class _InertProvider:
22
+ """Stands in for a real MCPClient so tests never touch the network."""
23
+
24
+ async def load_tools(self, **kwargs):
25
+ return []
26
+
27
+ def add_consumer(self, registry_id):
28
+ pass
29
+
30
+
31
+ def test_plugin_exposes_stable_name():
32
+ """The plugin exposes its stable name."""
33
+ plugin = YouDotComPlugin()
34
+ assert plugin.name == "youdotcom"
35
+
36
+
37
+ def test_without_api_key_only_keyless_servers_are_enabled(monkeypatch: pytest.MonkeyPatch):
38
+ """Without YDC_API_KEY, keyed servers are skipped and keyless ones remain."""
39
+ monkeypatch.delenv("YDC_API_KEY", raising=False)
40
+ plugin = YouDotComPlugin()
41
+ assert set(plugin.servers) == KEYLESS_SERVERS
42
+
43
+
44
+ def test_with_api_key_all_servers_are_enabled(monkeypatch: pytest.MonkeyPatch):
45
+ """With YDC_API_KEY (env or explicit), every shipped server is enabled."""
46
+ monkeypatch.setenv("YDC_API_KEY", "test-key")
47
+ from_env = YouDotComPlugin()
48
+ explicit = YouDotComPlugin(api_key="explicit-key")
49
+ assert set(from_env.servers) == ALL_SERVERS
50
+ assert set(explicit.servers) == ALL_SERVERS
51
+
52
+
53
+ class _FakeAgent:
54
+ """Captures tool providers the way the plugin registry would register them."""
55
+
56
+ def __init__(self) -> None:
57
+ self.providers: list[object] = []
58
+
59
+ class _Registry:
60
+ def __init__(self, outer: "_FakeAgent") -> None:
61
+ self._outer = outer
62
+
63
+ def process_tools(self, tools: list[object]) -> list[str]:
64
+ self._outer.providers.extend(tools)
65
+ return []
66
+
67
+ self.tool_registry = _Registry(self)
68
+
69
+
70
+ def test_init_agent_registers_one_client_per_enabled_server(monkeypatch: pytest.MonkeyPatch):
71
+ """init_agent hands one MCP client per enabled server to the tool registry,
72
+ with a Bearer header on keyed servers and no header on keyless ones."""
73
+ client_calls: list[dict] = []
74
+
75
+ def fake_mcp_client(_factory=None, *, url=None, headers=None, **_kwargs):
76
+ client_calls.append({"url": url, "headers": headers})
77
+ return object()
78
+
79
+ monkeypatch.setattr(plugin_mod, "MCPClient", fake_mcp_client)
80
+
81
+ plugin = YouDotComPlugin(api_key="test-key")
82
+ fake_agent = _FakeAgent()
83
+ plugin.init_agent(fake_agent) # type: ignore[arg-type]
84
+
85
+ assert len(fake_agent.providers) == len(ALL_SERVERS)
86
+ by_url = {call["url"]: call["headers"] for call in client_calls}
87
+ assert len(client_calls) == len(ALL_SERVERS)
88
+ for name, server in plugin.servers.items():
89
+ if name in KEYED_SERVERS:
90
+ assert by_url[server["url"]] == {"Authorization": "Bearer test-key"}
91
+ else:
92
+ assert by_url[server["url"]] is None
93
+
94
+
95
+ def test_attached_agent_discovers_bundled_skills_and_skills_tool(monkeypatch: pytest.MonkeyPatch):
96
+ """An agent with the plugin gains the skills tool and sees the bundled skills."""
97
+ monkeypatch.setattr(plugin_mod, "MCPClient", lambda *args, **kwargs: _InertProvider())
98
+
99
+ agent = Agent(plugins=[YouDotComPlugin()])
100
+
101
+ assert "skills" in agent.tool_registry.get_all_tools_config()
102
+ attached = agent._plugin_registry._plugins["youdotcom"]
103
+ # Skill discovery is lazy: the same load runs at each invocation via the
104
+ # BeforeInvocationEvent hook. Exercise that exact path here.
105
+ asyncio.run(attached._on_before_invocation(BeforeInvocationEvent(agent=agent)))
106
+ names = {skill.name for skill in attached.get_available_skills(agent)}
107
+ assert names == {"you-web", "you-research", "you-discover", "you-finance"}
108
+
109
+
110
+ def test_without_key_agent_system_prompt_notes_skipped_servers(monkeypatch: pytest.MonkeyPatch):
111
+ """Without a key the agent's system prompt says which servers are unavailable."""
112
+ monkeypatch.delenv("YDC_API_KEY", raising=False)
113
+ monkeypatch.setattr(plugin_mod, "MCPClient", lambda *args, **kwargs: _InertProvider())
114
+
115
+ agent = Agent(plugins=[YouDotComPlugin()])
116
+ attached = agent._plugin_registry._plugins["youdotcom"]
117
+ # The registry runs every callback registered for the event; replicate that
118
+ # ordering here: the inherited skills hook, then the capability-note hook.
119
+ asyncio.run(attached._on_before_invocation(BeforeInvocationEvent(agent=agent)))
120
+ asyncio.run(attached._on_before_invocation_note(BeforeInvocationEvent(agent=agent)))
121
+
122
+ assert "YDC_API_KEY" in agent.system_prompt
123
+ for name in KEYED_SERVERS: # servers the agent can NOT call
124
+ assert name in agent.system_prompt
125
+
126
+
127
+ def test_with_key_agent_system_prompt_has_no_capability_note(monkeypatch: pytest.MonkeyPatch):
128
+ """With a key present, no capability note is injected."""
129
+ monkeypatch.setenv("YDC_API_KEY", "test-key")
130
+ monkeypatch.setattr(plugin_mod, "MCPClient", lambda *args, **kwargs: _InertProvider())
131
+
132
+ agent = Agent(plugins=[YouDotComPlugin()])
133
+ attached = agent._plugin_registry._plugins["youdotcom"]
134
+ asyncio.run(attached._on_before_invocation(BeforeInvocationEvent(agent=agent)))
135
+ asyncio.run(attached._on_before_invocation_note(BeforeInvocationEvent(agent=agent)))
136
+
137
+ assert "YDC_API_KEY" not in agent.system_prompt