| name | nooa-tools-and-skills |
| description | Give a NOOA agent capabilities — methods as tools, built-in tools (ShellTools, TodoManager), MCP servers, agent skills (Skill/TextSkill/SkillRegistry), and multimodal media. Use when adding tools or external integrations to an agent, wiring MCP, or packaging reusable guidance as an agent skill. |
| compatibility | nooa package; [mcp] extra for MCP |
Tools, Skills, and Integrations
Methods are the tools
There is no tool-registration abstraction: any visible method or attribute on self is callable from CodeAct-generated code, with doc(self) telling the LLM what exists. Mix deterministic helpers (SW1) with generation methods (SW3):
class InventoryAgent(Agent, llm=llm):
"""Checks inventory using deterministic helpers."""
def get_stock(self, item: str) -> int:
return self.inventory.get(item, {}).get("stock", 0)
async def can_fulfill(self, items: list[str], budget: float) -> Result:
"""Check whether the order fits the budget. Use self.get_stock()."""
...
Design rubric: exact semantics (matching rules, parsing, arithmetic) → deterministic helper; fuzzy judgment → generation method. Give helpers precise names, type hints, and docstrings — those are their tool schema.
Built-in tools
from nooa.tools import ShellTools, TodoManager
class DevAgent(Agent, llm=llm):
def __init__(self):
super().__init__()
self.shell = ShellTools(cwd=".")
self.todos = TodoManager()
ShellTools is the one shell/file tool (it replaced the removed BashTool/FileTool). One persistent bash session — cd, export, and cwd survive across calls — and four methods:
r = await shell.run("grep -rn 'def foo' src/")
await shell.run("python -", stdin=script)
region = await shell.read("f.py", lines=(10, 25))
await shell.replace(r.matches[0], new_code)
await shell.write_file("out.py", content)
Tools attached as public attributes are automatically visible; the LLM discovers their APIs via doc(self.shell).
For an object the LLM might misuse, prefer wrapping the dangerous surface in a small deterministic method over exposing the raw object.
Agent skills (Skill / TextSkill / SkillRegistry)
Skills inject curated guidance + optional helper APIs into an agent. Three forms:
from nooa import Skill, TextSkill
class MyAgent(Agent, llm=llm):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.git = GitWorkflow()
self.pd = Skill(pd)
self.frontend = TextSkill(path=Path("skills/frontend-design"))
- Agents see a
# Skills block (one-liner per skill) in their execution context and call doc(self.<skill>) for the full guide.
TextSkill exposes read_file(path) and await run_script(name, *args) for files/scripts bundled with the SKILL.md.
- SKILL.md frontmatter: required
name, description; optional compatibility, metadata, user-invocable, allowed-tools — Claude-Code-compatible format.
SkillRegistry(agent) supports explicit register(...) / activate(...)
and bulk discover_skills_dirs(...). Register model-facing TextSkill
objects under a non-cmd.* name so the active Skills block advertises their
descriptions. Discovered SKILL.md directories use the cmd.* namespace for
host/user commands and are intentionally omitted from that model-facing
summary. SkillManager does not exist.
@slash_command on a Skill method marks it user-invocable via a host that reads the agent's slash_commands queue (see InteractiveAgent).
- Docstring conventions: see
skills/nooa-agent-authoring/SKILL.md and AGENTS.md.
MCP servers
Core library keeps MCP optional: uv sync --extra mcp (or uv add 'nooa[mcp]').
from nooa.mcp import MCPManager, MCPTool
class ConfluenceAgent(Agent, llm=llm):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.confluence: MCPTool = MCPManager.create_from_server("my-mcp-server")
async def respond(self, prompt: str) -> str:
"""Answer using the Confluence tool."""
...
MCP tools appear alongside regular methods — the LLM calls them like any other attribute. Servers are configured in .mcp.json (VS Code/Claude Code format). Transports: stdio, SSE, streamable-http; OAuth supported (nooa.mcp.OAuthConfig).
Each returned MCPTool owns a client/connection. Treat it as stateful and keep
it per agent instance, just like ShellTools.
Agent documentation parses __init__ and infers direct constructor assignments
such as self.shell = ShellTools(...) or self.frontend = TextSkill(...).
Annotate the assignment in place when a factory, injected parameter, or other
expression obscures the type, as in the MCP example above. Class-level
annotations are an optional, source-independent alternative. SkillRegistry
activation handles dynamically discovered skills explicitly.
${VAR} placeholders in .mcp.json and inline servers configuration remain
literal. If a server needs a secret, resolve it in trusted caller code and pass
the resulting URL, header, argument, or environment value directly to
MCPManager.create_from_server().
Multimodal media
from nooa import Image, Audio, Video, File
class MediaAgent(Agent, llm=llm):
async def describe(self, image: Image) -> str:
"""Describe what you see."""
...
img = Image.from_file("photo.png")
await MediaAgent().describe(img)
Only works with multimodal-capable models — text-only models error when handed media. See examples/quickstart/12_multimodal.py.
Pitfalls
- A tool attribute assigned as a bare function/lambda isn't introspectable — use class methods or proper objects.
- Skills/tools attached to
self are per-instance; create them in __init__
(after super().__init__()). An object assigned on the class is shared by
every agent instance.
- Hide internal-only tools with
Annotated[T, hidden] so they don't pollute every prompt.
- External API fragility: wrap flaky calls in a small deterministic method with clear errors, so CodeAct sees a clean failure it can react to.
Related skills
nooa-agent-authoring — visibility rules and method design that make tools discoverable.
nooa-capturing-traces — every tool invocation becomes a tool_execution.* span you can inspect.