create_skill
ChatGPTPersist a proven workflow as a reusable Skill. SAFETY: Does not overwrite or delete anything. This tool WRITES — it saves a new skill to your Mosofin skill library (and, when you choose that destination, sends it to Claude). Saving again never replaces an earlier skill: each save is kept as a new version and previous versions remain. It cannot edit or remove an existing skill, and it never touches your accounting or business data in any connected platform. It only runs after you explicitly confirm the save. PREREQUISITES: workspace confirmed via list_workspaces + the user's explicit, POST-RESULTS confirmation to save at all (native elicit accept, or confirmed="yes" — server-enforced, not just advisory; see below) + destination (required — ask the user in chat first) + `files= (the complete bundle; validated + secret-scanned). When destination includes mosofin, the bundle is pushed BLOCKING (45s timeout) to the Django backend's skill library (POST /api/mcp/skills/upload/) — no local write, no best-effort swallow. On any failure nothing was saved and a structured error envelope is returned. Args: name: Human-readable name. Used as the display title and to derive the kebab-case slug identifier. description: ~100-word description that triggers the skill in future conversations (e.g. "use when the user asks for ..."). destination: One of "mosofin" / "claude" / "both". REQUIRED — ask the user in plain chat first, then pass the answer here. (Inline elicit picker exists as a fallback but is unreliable in some clients — don't rely on it.) files: REQUIRED JSON-encoded mapping of {relative_path: text_content}. The server persists EXACTLY these files (Claude composes the bundle, including any custom artifacts like references/*.md or scripts/render.py). The bundle MUST include SKILL.md at its root with valid YAML frontmatter (name + description). Datasource-backed workflows MUST also include references/run-recipe.json — one step per invoke made during the chat (exact params, {workspace_id} placeholder, inputs declaring run-time values like dates with the question to ask the user, returns per payload, optional scripts/ transform) — enforced, not advisory. .html/.css/.svg files are rejected. Limits: ≤50 files, ≤1MB per file, ≤5MB total. Path-traversal (.., absolute paths, null bytes) rejected. COMPANY FILES (multi-entity): when the workspace can connect the same platform more than once — several QuickBooks companies, say — and this run read one of them, the recipe MUST record which. Declare the input:: {"name": "data_source_id", "type": "data_source", "connector_key": "quickbooks", "ask": "Which company should this run use?"} and put "data_source_id": "{data_source_id}" on every step that read it, so a replay asks once and reuses the answer. Use a LITERAL id there instead only when the user explicitly pinned this skill to one company (e.g. "Monthly close — Acme LLC"); the replay then never asks. Omit both when the platform can only ever have one connection. datasources: Comma-separated datasource ids the skill reads from — REQUIRED whenever the workflow invoked a datasource; must exactly match the run-recipe steps (enforced). Recorded as the skill's sources metadata. Returns SkillUploadResult with: - skill_id (backend hashid) + name (always) - files_committed listing every pushed file with its SHA-256 (computed client-side from the pushed bundle content) - claude_bundle (when claude in destinations) — file tree + install instructions for Claude Code / Claude.ai On a backend failure (any ValidAccessError), returns a structured error envelope dict instead — nothing was persisted. Save confirmation gate: mirrors get_my_skill's consent pattern (server-enforced, not just advisory). Show the user the completed results/analysis FIRST, then ask "save this as a reusable skill?" as its own question — a general earlier go-ahead ("yes, run it and save it") is not a substitute and must not be passed through as confirmed="yes"`. Runs…
get_agent_datasources
ChatGPTList the LIVE datasources connected in a Mosofin workspace. SAFETY: Does not change any of your data. This tool only reports which platforms and company files are connected and whether each connection is live. It does not connect, disconnect, or reauthorize anything, and it does not read business data from the platforms themselves. This is the live "what is ACTUALLY connected" view, read straight from the Django backend. It is the counterpart to `get_datasource_tools (which is the STATIC "what is possible" catalog view). Each datasource carries its raw status plus a derived connected flag (true iff the status is ACTIVE, case-tolerant), so the model can tell the user whether a platform is really wired up before attempting to invoke any of its tools. Returns EVERY datasource in the workspace — including disconnected/retired ones (which surface with connected: false and a null agent_slug). agent_id is an OPTIONAL best-effort filter (the agent's catalog id, e.g. 'bea'): when given, the list is narrowed to that agent's datasources BUT unlinked/retired rows (no agent attribution) are still surfaced so a stale connection is never hidden. Omit agent_id for the full per-tenant list. PRESENTING THE ANSWER: read agents — it is already grouped and sorted the way the user wants it shown: each agent by name, under it each platform by name, and under a platform with several accounts the list of account names. A platform with one account carries that account's name and status inline. datasources is the same data as a flat index. MULTIPLE COMPANIES: a workspace can connect the same platform more than once — several QuickBooks company files, for example. Each is a separate row here with its own data_source_id. When more than one row for a platform is connected, ASK THE USER which company they mean and pass that row's data_source_id on every invoke_datasource_api_tool and get_datasource_tools call. Refer to companies by display_name; never show the raw data_source_id. To compare companies, invoke once per company and label each result. (Invoking without a choice when several are live returns an entity_required error listing them.) Resolution order for workspace_id`: tool arg → JWT claim → elicit.
get_datasource_tools
ChatGPTList the API tools available on a datasource, each with its policy. SAFETY: Does not change any of your data. This tool only describes which read tools exist on a connected platform and whether you are allowed to use them. It does not run any of them and does not contact the platform's data API. PREREQUISITES: workspace confirmed. Agent-permission gate applies (agent must include the datasource in its permitted set). This is the ONLY way to surface per-datasource API tools to the client — they are intentionally hidden from the top-level `list_tools to keep the surface focused on workflow navigation. Returns a structured list of tools each with: short name (e.g. 'list_invoices'), qualified_name (e.g. 'quickbooks.list_invoices') matching the Skill.tools references, description, and effective_policy (enabled / permission / disabled). Use this to discover which tools exist AND their policies before invoking. A tool with effective_policy: "permission" is still callable — call invoke_datasource_api_tool and the server prompts the user for one-time approval (native ctx.elicit dialog when the transport supports it; otherwise an approval_required envelope for chat consent). Only disabled means do not invoke. The candidate tool SET is sourced from the live backend api_tools_tool MENU (the product owner's source of truth for what a connector exposes), NOT the static catalog. Before exposure, every candidate passes through the canonical read-only operation filter. Write-capable tools (e.g. create_journal) are hidden from this MCP surface even if the backend menu contains them. Each remaining tool's effective_policy is resolved override-else-default: a per-workspace api_tools_apitoolpolicy override (keyed on the tool id) beats the tool's factory default_policy. ACTIVE-gated: the listing is EMPTY when there is no live connection. If the workspace has no ACTIVE agents_datasource row for the connector, there is nothing to configure, so api_tools is [] (see get_agent_datasources for the live connection view). The agent-permission check is strict: passing a datasource the agent does not include in its datasources[] raises ValueError` so the model self-corrects rather than calling a forbidden datasource. Resolution: workspace_id → tool arg / JWT / elicit. agent_id → tool arg / elicit. datasource → tool arg (validated) / elicit (limited to the agent's permitted set).
get_my_skill
ChatGPTFetch one saved skill's bundle (manifest, or specific files in full). SAFETY: Does not change any of your data. This tool only opens a skill you already saved so its steps can be read. It does not edit, re-save, or delete the skill, does not run the steps inside it, and never touches a connected platform. It only runs after you confirm. PREREQUISITES: workspace confirmed via list_workspaces + skill_id (owner-only) + user confirmation (native elicit accept, or confirmed="yes" after an explicit chat yes). The consent gate is server-enforced, not just advisory: once get_skills returns a list, the model cannot silently replay a skill -- the user must explicitly approve it first. A refused call (elicit-unsupported, declined, cancelled, or explicit "no") returns no bundle content. Default (no `paths): returns the skill's metadata, SKILL.md body (always inline), and a MANIFEST of every bundled resource -- small text/markdown references and references/run-recipe.json are inlined (<=8KB per file, 48KB total budget); everything else (assets, oversize references) is omitted (content_omitted=True). Re-call with paths=[...], confirmed="yes" to fetch those files' FULL content (no size cap) -- the result's resources then contain only the requested entries. An unknown path raises ValueError listing the valid paths. REPLAY: when the manifest contains references/run-recipe.json, replay by FIRST asking the user each of its inputs entries (put the ask question verbatim -- dates are never silently defaulted), then executing the steps in order (same-parallel_group steps batched in one message), substituting {workspace_id} and the collected inputs into each step's params. A type: "data_source" input means WHICH connected company file the run should read. Resolve it by calling get_agent_datasources and asking the user to pick by display_name (never show the raw id); if exactly one is connected for that connector_key you may use it without asking. Pass the chosen id as the data_source_id argument of every invoke_datasource_api_tool call whose step carries "data_source_id": "{data_source_id}"`. A step with a LITERAL id instead of the placeholder is deliberately pinned to one company -- use it as written and do not re-ask. Owner-only: a skill owned by someone else, archived, unknown, or a malformed id all return the SAME error — never infer existence from it.
get_skills
ChatGPTList the caller's own saved skills, scoped to the resolved workspace. SAFETY: Does not change any of your data. This tool only lists the skills you have already saved. It does not create, edit, run, or delete a skill, and it never touches a connected platform. PREREQUISITES: workspace confirmed via list_workspaces. OPTIONAL discovery — invoke_datasource_api_tool does NOT require this call first. When to call: When the user asks what skills are available, whether a saved workflow covers their ask, or describes a repeatable task (e.g. "expense dashboard for June"). Match rows by `name and description. Optional dashboard_slug on a row is an ignored legacy field from the backend — do not route to show-tools; always replay via get_my_skill + SKILL.md + invoke_datasource_api_tool. On a confident match, present the skill to the user BY NAME and get their explicit yes before replaying it — the user decides, never auto-replay. Only after that explicit yes, call get_my_skill(skill_id, confirmed="yes") next and follow SKILL.md directly. When no skill matches (or the list is empty), answer directly via invoke_datasource_api_tool. Backend-stored (docs/django-api/10-skills-api-handoff-2026-07-03.md), NOT a static catalog: sourced live from GET /api/mcp/skills/. Skills are workspace-scoped (D56, docs/django-api/11-create-skill-cutover-handoff-2026-07-09.md §1.3) — this returns only skills where owner = JWT user AND tenant = the gated workspace; identity is (tenant, owner, name, data_source). A skill saved in one workspace is NOT listed under another workspace. Each row carries skill_id (opaque hashid — feed into get_my_skill for the full bundle), plus creator, agents[], datasources[], enabled, visibility, review_status, version. Returns a PersonalSkillList shape (dumped to a dict — the return annotation stays dict[str, Any] because the strict schema type lives in app.mcp.skills.schemas, which cannot be imported at THIS module's top level without re-entering the app.mcp.skills package-init cycle; see the local import below) on success, or a structured error envelope (to_error_envelope`) on any backend/gate failure.
invoke_datasource_api_tool
ChatGPTInvoke a per-datasource API tool to read business data. SAFETY: Does not change any of your business data. This tool only reads from the platforms connected to your workspace — QuickBooks, Stripe and the rest. It cannot create, edit, delete, send, or pay anything: the server refuses any operation that is not a read, so no accounting record, invoice, payment, or customer is ever modified. It DOES reach out over the internet to those third-party platforms to fetch your data, which is why it is marked open-world. PREREQUISITES: workspace confirmed via list_workspaces — that is the ONLY workflow gate. Per-call agent-permission / live-status / policy gates still apply. Multiple calls to this tool may run IN PARALLEL — batch independent reads (e.g. P&L + balance sheet + aging) as concurrent invocations instead of serializing them. Optionally call `get_skills first to check whether a saved skill covers the user's ask, and get_datasource_tools to discover valid tool names — neither is required before invoking. QuickBooks is a LIVE, read-only connector: responses come from the user's real QuickBooks Online company via the Intuit REST API (using the workspace's stored OAuth connection) and are stamped "mock": false. Other datasources may still be fixture-backed ("mock": true) until they are wired live — always check the mock field before telling the user whether numbers are real. Mosofin is strictly READ-ONLY: no create / update / delete tool is exposed on any datasource (PERM-05). GROUNDING (enforced contract, echoed in every result): every figure you give the user must come from data returned by this tool in this conversation — do NOT tag individual figures with source citations; instead end each data-backed answer with a single 'Data sources' line grouping the calls used by datasource (each result carries a provenance block — datasource, tool, params, fetched_at, mock). If the fetched data does not cover part of the question, say so and name the tool that could fetch it instead of estimating. Routing: - datasource: the platform id (e.g. "quickbooks"). - tool_name: the API tool's plain name (e.g. "search_invoices"). Valid names come from the workspace's connector catalog / conversation context; an UNKNOWN_TOOL error also lists the valid names. - params: a JSON object of tool-specific filters (e.g. {"start_date": "2025-12-01", "end_date": "2025-12-31", "max_results": 50}). - approved: set True ONLY after the user has EXPLICITLY approved (in chat) a tool whose policy is permission. The model relays the user's consent via this flag — there is NO native human-in-the-loop channel in this server's stateless transport, so a permission tool first returns an approval_required envelope; ask the user, and on their explicit yes re-invoke this SAME tool with approved=True. Never set it without the user's explicit yes. Pagination for search/query-kind tools: a QuickBooks search_* tool's result is shaped as {"rows": [...], "pagination": {"offset", "max_results", "start_position", "total_count", "has_more", "next_offset"}}. Once a date range is set, the server returns as many rows as fit under the response size cap per call (up to 1000 rows/page — QBO's hard maximum). If a page is too large, the server automatically shrinks the page size and retries internally, so you do NOT need to manually guess a small max_results defensively — omit max_results entirely to get the largest page that fits. When pagination.has_more is true, re-invoke this SAME tool with params.offset set to the previous response's pagination.next_offset to fetch the next page; pagination.max_results reflects the ACTUAL page size used, which may be smaller than requested if the server had to shrink it. Budgets — two-step flow: first call search_budgets to LIST which budgets exist (lightweight, headers + names). Then call get_budget_details with budget_id or budget_name` to get the NUMBERS for exactly one budget — it always returns full BudgetDetail li…
list_workspaces
ChatGPTList or confirm the workspace(s) the current user can access. SAFETY: Does not change any of your data. This tool only reads the list of workspaces you already belong to and remembers which one you picked for this conversation. It creates nothing, edits nothing, and deletes nothing — in Mosofin or in any connected platform. PREREQUISITES: none — the root tool, always safe to call first. Records workspace confirmation — the only gate before data tools. Covers active memberships in non-archived tenants, deterministically ordered by membership.created_at (WR-04). When the user has one workspace, this tool auto-confirms it. When they have multiple, the first call (no confirm args) returns `selection_required — ask in chat whether the task is **single-** or **multi-workspace**, then ask which workspace(s) by name, then call again with workspace_ids + mode before any other Mosofin tool. Each entry's workspace_id is an OPAQUE handle (ws_…), NOT the raw integer tenant PK — pass it straight back as the workspace_id` arg to any tool (Phase E: the raw id never crosses to the client).