nocfo-cli 1.5.2__tar.gz → 1.7.2__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.
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/PKG-INFO +1 -1
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/pyproject.toml +1 -1
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/app.py +2 -16
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/config.py +4 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/auth.py +30 -15
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/bookkeeping/account.py +37 -35
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/bookkeeping/document.py +50 -44
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/bookkeeping/header.py +19 -10
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/bookkeeping/relation.py +25 -68
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/bookkeeping/tag_file.py +67 -47
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/common.py +19 -19
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/constants/docs.py +13 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/invoicing/contact.py +45 -26
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/invoicing/product.py +31 -21
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/invoicing/purchase_invoice.py +25 -18
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/invoicing/sales_invoice.py +49 -44
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/reporting/report.py +85 -23
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/runtime.py +0 -19
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/bookkeeping/document.py +15 -27
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/common.py +0 -6
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/invoicing/contact.py +70 -8
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/invoicing/sales_invoice.py +16 -3
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/server.py +21 -18
- nocfo_cli-1.5.2/src/nocfo_toolkit/mcp/curated/confirmation.py +0 -137
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/LICENSE +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/README.md +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/__init__.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/api_client.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/__init__.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/commands/__init__.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/commands/_helpers.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/commands/accounts.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/commands/auth.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/commands/businesses.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/commands/contacts.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/commands/documents.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/commands/files.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/commands/invoices.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/commands/products.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/commands/purchase_invoices.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/commands/reports.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/commands/tags.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/commands/user.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/context.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/cli/output.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/__init__.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/__init__.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/bookkeeping/__init__.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/client.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/constants/__init__.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/errors.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/instructions.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/invoicing/__init__.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/reporting/__init__.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/__init__.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/bookkeeping/__init__.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/bookkeeping/account.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/bookkeeping/header.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/bookkeeping/tag_file.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/constants/__init__.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/constants/docs.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/invoicing/__init__.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/invoicing/product.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/invoicing/purchase_invoice.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/reporting/__init__.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schema/reporting/report.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/schemas.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/curated/utils.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/error_handling.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/http_error_capture.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/middleware.py +0 -0
- {nocfo_cli-1.5.2 → nocfo_cli-1.7.2}/src/nocfo_toolkit/mcp/search.py +0 -0
|
@@ -121,19 +121,12 @@ def run_mcp_server(
|
|
|
121
121
|
"context by exposing search_tools + call_tool."
|
|
122
122
|
),
|
|
123
123
|
),
|
|
124
|
-
skip_confirmation: bool = typer.Option(
|
|
125
|
-
False,
|
|
126
|
-
"--skip-confirmation/--require-confirmation",
|
|
127
|
-
help=(
|
|
128
|
-
"Skip interactive mutation confirmation elicitation. "
|
|
129
|
-
"Use only in trusted automation environments."
|
|
130
|
-
),
|
|
131
|
-
),
|
|
132
124
|
) -> None:
|
|
133
125
|
"""Run NoCFO MCP server over stdio or HTTP transport.
|
|
134
126
|
|
|
135
127
|
Stdio mode accepts either NOCFO_JWT_TOKEN or NOCFO_API_TOKEN.
|
|
136
|
-
Optional NOCFO_CLIENT overrides default `nocfo-mcp` x-nocfo-client.
|
|
128
|
+
Optional NOCFO_CLIENT overrides default `nocfo-mcp` x-nocfo-client header.
|
|
129
|
+
Use `nocfo-agent` for internal agent MCP and `nocfo-mcp` for external MCP.
|
|
137
130
|
HTTP oauth mode uses connector bearer verification + JWT exchange flow.
|
|
138
131
|
"""
|
|
139
132
|
|
|
@@ -173,12 +166,6 @@ def run_mcp_server(
|
|
|
173
166
|
if env_tool_search
|
|
174
167
|
else tool_search
|
|
175
168
|
)
|
|
176
|
-
env_skip_confirmation = os.getenv("NOCFO_MCP_SKIP_CONFIRMATION", "").strip().lower()
|
|
177
|
-
skip_confirmation_enabled = (
|
|
178
|
-
env_skip_confirmation in {"1", "true", "yes", "on"}
|
|
179
|
-
if env_skip_confirmation
|
|
180
|
-
else skip_confirmation
|
|
181
|
-
)
|
|
182
169
|
|
|
183
170
|
options = MCPServerOptions(
|
|
184
171
|
auth_mode=auth_mode_value,
|
|
@@ -186,7 +173,6 @@ def run_mcp_server(
|
|
|
186
173
|
required_scopes=scope_items,
|
|
187
174
|
stateless_http=stateless_http,
|
|
188
175
|
tool_search=tool_search_enabled,
|
|
189
|
-
skip_confirmation=skip_confirmation_enabled,
|
|
190
176
|
)
|
|
191
177
|
|
|
192
178
|
if transport_normalized == "http":
|
|
@@ -20,6 +20,10 @@ except ModuleNotFoundError: # pragma: no cover - fallback for minimal environme
|
|
|
20
20
|
DEFAULT_BASE_URL = "https://api-prd.nocfo.io"
|
|
21
21
|
AUTH_HEADER_SCHEME = "Token"
|
|
22
22
|
MIN_PAT_LENGTH = 8
|
|
23
|
+
NOCFO_CLIENT_HEADER = "x-nocfo-client"
|
|
24
|
+
NOCFO_CLIENT_MCP = "nocfo-mcp"
|
|
25
|
+
NOCFO_CLIENT_AGENT = "nocfo-agent"
|
|
26
|
+
NOCFO_CLIENT_DEFAULT = NOCFO_CLIENT_MCP
|
|
23
27
|
|
|
24
28
|
|
|
25
29
|
class OutputFormat(str, Enum):
|
|
@@ -23,7 +23,12 @@ from starlette.datastructures import MutableHeaders
|
|
|
23
23
|
from starlette.responses import JSONResponse, Response
|
|
24
24
|
from starlette.routing import Route
|
|
25
25
|
|
|
26
|
-
from nocfo_toolkit.config import
|
|
26
|
+
from nocfo_toolkit.config import (
|
|
27
|
+
AUTH_HEADER_SCHEME,
|
|
28
|
+
NOCFO_CLIENT_DEFAULT,
|
|
29
|
+
NOCFO_CLIENT_HEADER,
|
|
30
|
+
ToolkitConfig,
|
|
31
|
+
)
|
|
27
32
|
|
|
28
33
|
logger = logging.getLogger(__name__)
|
|
29
34
|
|
|
@@ -47,11 +52,21 @@ def _split_csv(raw: str | None) -> list[str]:
|
|
|
47
52
|
|
|
48
53
|
|
|
49
54
|
def _incoming_mcp_client() -> str | None:
|
|
50
|
-
headers = get_http_headers(include={
|
|
51
|
-
nocfo_client = (headers.get(
|
|
55
|
+
headers = get_http_headers(include={NOCFO_CLIENT_HEADER})
|
|
56
|
+
nocfo_client = (headers.get(NOCFO_CLIENT_HEADER) or "").strip()
|
|
52
57
|
return nocfo_client or None
|
|
53
58
|
|
|
54
59
|
|
|
60
|
+
def resolve_nocfo_client(*, default_client: str | None = None) -> str:
|
|
61
|
+
"""Resolve x-nocfo-client from incoming MCP request or configured default."""
|
|
62
|
+
incoming_client = _incoming_mcp_client()
|
|
63
|
+
if incoming_client:
|
|
64
|
+
return incoming_client
|
|
65
|
+
if default_client:
|
|
66
|
+
return default_client
|
|
67
|
+
return NOCFO_CLIENT_DEFAULT
|
|
68
|
+
|
|
69
|
+
|
|
55
70
|
@dataclass(frozen=True)
|
|
56
71
|
class MCPAuthOptions:
|
|
57
72
|
"""Runtime auth settings used by the MCP server."""
|
|
@@ -265,11 +280,13 @@ class JwtExchangeAuth(httpx.Auth):
|
|
|
265
280
|
*,
|
|
266
281
|
exchange_path: str = "/auth/jwt/",
|
|
267
282
|
refresh_skew_seconds: int = 60,
|
|
283
|
+
default_client: str | None = None,
|
|
268
284
|
) -> None:
|
|
269
285
|
self._exchange_path = (
|
|
270
286
|
exchange_path if exchange_path.startswith("/") else f"/{exchange_path}"
|
|
271
287
|
)
|
|
272
288
|
self._refresh_skew_seconds = max(0, refresh_skew_seconds)
|
|
289
|
+
self._default_client = default_client
|
|
273
290
|
self._cache: dict[str, tuple[str, int | None]] = {}
|
|
274
291
|
self._locks: dict[str, asyncio.Lock] = {}
|
|
275
292
|
|
|
@@ -312,7 +329,7 @@ class JwtExchangeAuth(httpx.Auth):
|
|
|
312
329
|
bearer_token = access.token if access else None
|
|
313
330
|
if not bearer_token:
|
|
314
331
|
raise RuntimeError("Missing OAuth bearer token for MCP request.")
|
|
315
|
-
|
|
332
|
+
nocfo_client = resolve_nocfo_client(default_client=self._default_client)
|
|
316
333
|
|
|
317
334
|
claims = access.claims if access and isinstance(access.claims, dict) else {}
|
|
318
335
|
cache_key = self._cache_key(bearer_token, claims)
|
|
@@ -332,11 +349,7 @@ class JwtExchangeAuth(httpx.Auth):
|
|
|
332
349
|
"Authorization": f"Bearer {bearer_token}",
|
|
333
350
|
"Content-Type": "application/json",
|
|
334
351
|
"Accept": "application/json",
|
|
335
|
-
|
|
336
|
-
{"x-nocfo-client": incoming_nocfo_client}
|
|
337
|
-
if incoming_nocfo_client
|
|
338
|
-
else {}
|
|
339
|
-
),
|
|
352
|
+
NOCFO_CLIENT_HEADER: nocfo_client,
|
|
340
353
|
},
|
|
341
354
|
content=b"{}",
|
|
342
355
|
)
|
|
@@ -382,23 +395,25 @@ class JwtExchangeAuth(httpx.Auth):
|
|
|
382
395
|
self._cache[cache_key] = (jwt_token, self._decode_exp(jwt_token))
|
|
383
396
|
|
|
384
397
|
request.headers["Authorization"] = f"{AUTH_HEADER_SCHEME} {jwt_token}"
|
|
385
|
-
|
|
386
|
-
request.headers["x-nocfo-client"] = incoming_nocfo_client
|
|
398
|
+
request.headers[NOCFO_CLIENT_HEADER] = nocfo_client
|
|
387
399
|
yield request
|
|
388
400
|
|
|
389
401
|
|
|
390
402
|
class PassthroughAuth(httpx.Auth):
|
|
391
403
|
"""Forward incoming HTTP Authorization header to downstream API requests."""
|
|
392
404
|
|
|
405
|
+
def __init__(self, *, default_client: str | None = None) -> None:
|
|
406
|
+
self._default_client = default_client
|
|
407
|
+
|
|
393
408
|
async def async_auth_flow(self, request: httpx.Request):
|
|
394
|
-
headers = get_http_headers(include={"authorization",
|
|
409
|
+
headers = get_http_headers(include={"authorization", NOCFO_CLIENT_HEADER})
|
|
395
410
|
authorization = headers.get("authorization")
|
|
396
411
|
if not authorization:
|
|
397
412
|
raise RuntimeError("Missing Authorization header for MCP passthrough mode.")
|
|
398
413
|
request.headers["Authorization"] = authorization
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
414
|
+
request.headers[NOCFO_CLIENT_HEADER] = resolve_nocfo_client(
|
|
415
|
+
default_client=self._default_client
|
|
416
|
+
)
|
|
402
417
|
yield request
|
|
403
418
|
|
|
404
419
|
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from fastmcp.tools import tool
|
|
8
|
-
from
|
|
8
|
+
from fastmcp.tools.tool import ToolAnnotations
|
|
9
9
|
from nocfo_toolkit.mcp.curated.runtime import business_slug, get_client
|
|
10
10
|
from nocfo_toolkit.mcp.curated.schemas import (
|
|
11
11
|
AccountActionInput,
|
|
@@ -42,6 +42,12 @@ account_fields = (
|
|
|
42
42
|
|
|
43
43
|
@tool(
|
|
44
44
|
name="bookkeeping_accounts_list",
|
|
45
|
+
annotations=ToolAnnotations(
|
|
46
|
+
readOnlyHint=True,
|
|
47
|
+
destructiveHint=False,
|
|
48
|
+
idempotentHint=True,
|
|
49
|
+
openWorldHint=False,
|
|
50
|
+
),
|
|
45
51
|
description="List bookkeeping accounts by account number, account range, name query, type, usage, or visibility. Use account numbers when talking with users.",
|
|
46
52
|
output_schema=ListEnvelope[AccountListItem].model_json_schema(),
|
|
47
53
|
)
|
|
@@ -63,6 +69,12 @@ async def bookkeeping_accounts_list(params: AccountListInput) -> dict[str, Any]:
|
|
|
63
69
|
|
|
64
70
|
@tool(
|
|
65
71
|
name="bookkeeping_account_retrieve",
|
|
72
|
+
annotations=ToolAnnotations(
|
|
73
|
+
readOnlyHint=True,
|
|
74
|
+
destructiveHint=False,
|
|
75
|
+
idempotentHint=True,
|
|
76
|
+
openWorldHint=False,
|
|
77
|
+
),
|
|
66
78
|
description="Retrieve one account from bookkeeping_accounts_list.items[].tool_handle.",
|
|
67
79
|
)
|
|
68
80
|
async def bookkeeping_account_retrieve(
|
|
@@ -84,21 +96,18 @@ async def bookkeeping_account_retrieve(
|
|
|
84
96
|
|
|
85
97
|
@tool(
|
|
86
98
|
name="bookkeeping_account_create",
|
|
99
|
+
annotations=ToolAnnotations(
|
|
100
|
+
readOnlyHint=False,
|
|
101
|
+
destructiveHint=False,
|
|
102
|
+
idempotentHint=False,
|
|
103
|
+
openWorldHint=False,
|
|
104
|
+
),
|
|
87
105
|
description="Create a bookkeeping account. Use account numbers and account names that match the user request.",
|
|
88
106
|
)
|
|
89
107
|
async def bookkeeping_account_create(params: PayloadInput) -> dict[str, Any]:
|
|
90
108
|
args = params
|
|
91
109
|
slug = await business_slug(args.business)
|
|
92
110
|
path = f"/v1/business/{slug}/account/"
|
|
93
|
-
await confirm_mutation(
|
|
94
|
-
business=slug,
|
|
95
|
-
tool_name="bookkeeping_account_create",
|
|
96
|
-
target_resource={
|
|
97
|
-
"type": "account",
|
|
98
|
-
"id": str(args.payload.get("number") or args.payload.get("name") or "new"),
|
|
99
|
-
},
|
|
100
|
-
parameters=args.payload,
|
|
101
|
-
)
|
|
102
111
|
result = await get_client().request(
|
|
103
112
|
"POST",
|
|
104
113
|
path,
|
|
@@ -110,6 +119,12 @@ async def bookkeeping_account_create(params: PayloadInput) -> dict[str, Any]:
|
|
|
110
119
|
|
|
111
120
|
@tool(
|
|
112
121
|
name="bookkeeping_account_update",
|
|
122
|
+
annotations=ToolAnnotations(
|
|
123
|
+
readOnlyHint=False,
|
|
124
|
+
destructiveHint=False,
|
|
125
|
+
idempotentHint=False,
|
|
126
|
+
openWorldHint=False,
|
|
127
|
+
),
|
|
113
128
|
description="Update a bookkeeping account selected by account_number.",
|
|
114
129
|
)
|
|
115
130
|
async def bookkeeping_account_update(params: AccountPayloadInput) -> dict[str, Any]:
|
|
@@ -122,15 +137,6 @@ async def bookkeeping_account_update(params: AccountPayloadInput) -> dict[str, A
|
|
|
122
137
|
business_slug=slug,
|
|
123
138
|
)
|
|
124
139
|
path = f"/v1/business/{slug}/account/{account_id}/"
|
|
125
|
-
await confirm_mutation(
|
|
126
|
-
business=slug,
|
|
127
|
-
tool_name="bookkeeping_account_update",
|
|
128
|
-
target_resource={
|
|
129
|
-
"type": "account",
|
|
130
|
-
"id": account_id,
|
|
131
|
-
},
|
|
132
|
-
parameters=args.payload,
|
|
133
|
-
)
|
|
134
140
|
result = await get_client().request(
|
|
135
141
|
"PATCH",
|
|
136
142
|
path,
|
|
@@ -142,6 +148,12 @@ async def bookkeeping_account_update(params: AccountPayloadInput) -> dict[str, A
|
|
|
142
148
|
|
|
143
149
|
@tool(
|
|
144
150
|
name="bookkeeping_account_delete",
|
|
151
|
+
annotations=ToolAnnotations(
|
|
152
|
+
readOnlyHint=False,
|
|
153
|
+
destructiveHint=True,
|
|
154
|
+
idempotentHint=False,
|
|
155
|
+
openWorldHint=False,
|
|
156
|
+
),
|
|
145
157
|
description="Delete a bookkeeping account selected by account_number.",
|
|
146
158
|
)
|
|
147
159
|
async def bookkeeping_account_delete(params: AccountNumberInput) -> dict[str, Any]:
|
|
@@ -154,14 +166,6 @@ async def bookkeeping_account_delete(params: AccountNumberInput) -> dict[str, An
|
|
|
154
166
|
business_slug=slug,
|
|
155
167
|
)
|
|
156
168
|
path = f"/v1/business/{slug}/account/{account_id}/"
|
|
157
|
-
await confirm_mutation(
|
|
158
|
-
business=slug,
|
|
159
|
-
tool_name="bookkeeping_account_delete",
|
|
160
|
-
target_resource={
|
|
161
|
-
"type": "account",
|
|
162
|
-
"id": account_id,
|
|
163
|
-
},
|
|
164
|
-
)
|
|
165
169
|
await get_client().request(
|
|
166
170
|
"DELETE",
|
|
167
171
|
path,
|
|
@@ -172,6 +176,12 @@ async def bookkeeping_account_delete(params: AccountNumberInput) -> dict[str, An
|
|
|
172
176
|
|
|
173
177
|
@tool(
|
|
174
178
|
name="bookkeeping_account_action",
|
|
179
|
+
annotations=ToolAnnotations(
|
|
180
|
+
readOnlyHint=False,
|
|
181
|
+
destructiveHint=False,
|
|
182
|
+
idempotentHint=False,
|
|
183
|
+
openWorldHint=False,
|
|
184
|
+
),
|
|
175
185
|
description="Show or hide a bookkeeping account selected by account_number.",
|
|
176
186
|
)
|
|
177
187
|
async def bookkeeping_account_action(params: AccountActionInput) -> dict[str, Any]:
|
|
@@ -184,14 +194,6 @@ async def bookkeeping_account_action(params: AccountActionInput) -> dict[str, An
|
|
|
184
194
|
business_slug=slug,
|
|
185
195
|
)
|
|
186
196
|
path = f"/v1/business/{slug}/account/{account_id}/{args.action.value}/"
|
|
187
|
-
await confirm_mutation(
|
|
188
|
-
business=slug,
|
|
189
|
-
tool_name="bookkeeping_account_action",
|
|
190
|
-
target_resource={
|
|
191
|
-
"type": "account",
|
|
192
|
-
"id": account_id,
|
|
193
|
-
},
|
|
194
|
-
)
|
|
195
197
|
await get_client().request(
|
|
196
198
|
"POST",
|
|
197
199
|
path,
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from fastmcp.tools import tool
|
|
8
|
-
from
|
|
8
|
+
from fastmcp.tools.tool import ToolAnnotations
|
|
9
9
|
from nocfo_toolkit.mcp.curated.runtime import business_slug, get_client
|
|
10
10
|
from nocfo_toolkit.mcp.curated.errors import raise_tool_error
|
|
11
11
|
from nocfo_toolkit.mcp.curated.schemas import (
|
|
@@ -34,7 +34,13 @@ from nocfo_toolkit.mcp.curated.utils import decode_tool_handle, items
|
|
|
34
34
|
|
|
35
35
|
@tool(
|
|
36
36
|
name="bookkeeping_documents_list",
|
|
37
|
-
|
|
37
|
+
annotations=ToolAnnotations(
|
|
38
|
+
readOnlyHint=True,
|
|
39
|
+
destructiveHint=False,
|
|
40
|
+
idempotentHint=True,
|
|
41
|
+
openWorldHint=False,
|
|
42
|
+
),
|
|
43
|
+
description="List bookkeeping documents by document number, dates, contact, tag, account number, VAT code/rate, workflow state, or query.",
|
|
38
44
|
output_schema=ListEnvelope[DocumentListItem].model_json_schema(),
|
|
39
45
|
)
|
|
40
46
|
async def bookkeeping_documents_list(
|
|
@@ -68,6 +74,12 @@ async def bookkeeping_documents_list(
|
|
|
68
74
|
|
|
69
75
|
@tool(
|
|
70
76
|
name="bookkeeping_document_retrieve",
|
|
77
|
+
annotations=ToolAnnotations(
|
|
78
|
+
readOnlyHint=True,
|
|
79
|
+
destructiveHint=False,
|
|
80
|
+
idempotentHint=True,
|
|
81
|
+
openWorldHint=False,
|
|
82
|
+
),
|
|
71
83
|
description="Retrieve one bookkeeping document from bookkeeping_documents_list.items[].tool_handle. Includes blueprint/entry/relation workflow summaries.",
|
|
72
84
|
)
|
|
73
85
|
async def bookkeeping_document_retrieve(
|
|
@@ -96,6 +108,12 @@ async def bookkeeping_document_retrieve(
|
|
|
96
108
|
|
|
97
109
|
@tool(
|
|
98
110
|
name="bookkeeping_document_create",
|
|
111
|
+
annotations=ToolAnnotations(
|
|
112
|
+
readOnlyHint=False,
|
|
113
|
+
destructiveHint=False,
|
|
114
|
+
idempotentHint=False,
|
|
115
|
+
openWorldHint=False,
|
|
116
|
+
),
|
|
99
117
|
description="Create a bookkeeping document/business transaction. Blueprint is the editable posting plan; generated entries are returned for verification.",
|
|
100
118
|
)
|
|
101
119
|
async def bookkeeping_document_create(
|
|
@@ -112,15 +130,6 @@ async def bookkeeping_document_create(
|
|
|
112
130
|
)
|
|
113
131
|
body = await resolve_document_payload(slug, args.payload, is_patch=False)
|
|
114
132
|
path = f"/v1/business/{slug}/document/"
|
|
115
|
-
await confirm_mutation(
|
|
116
|
-
business=slug,
|
|
117
|
-
tool_name="bookkeeping_document_create",
|
|
118
|
-
target_resource={
|
|
119
|
-
"type": "document",
|
|
120
|
-
"id": str(body.get("number") or body.get("description") or "new"),
|
|
121
|
-
},
|
|
122
|
-
parameters=body,
|
|
123
|
-
)
|
|
124
133
|
created = await get_client().request(
|
|
125
134
|
"POST",
|
|
126
135
|
path,
|
|
@@ -141,6 +150,12 @@ async def bookkeeping_document_create(
|
|
|
141
150
|
|
|
142
151
|
@tool(
|
|
143
152
|
name="bookkeeping_document_update",
|
|
153
|
+
annotations=ToolAnnotations(
|
|
154
|
+
readOnlyHint=False,
|
|
155
|
+
destructiveHint=False,
|
|
156
|
+
idempotentHint=False,
|
|
157
|
+
openWorldHint=False,
|
|
158
|
+
),
|
|
144
159
|
description="Update a document blueprint or metadata by document_number. This recalculates generated entries. If payload contains tag_names, those tags must already exist; create missing tags first with bookkeeping_tag_create.",
|
|
145
160
|
)
|
|
146
161
|
async def bookkeeping_document_update(
|
|
@@ -151,15 +166,6 @@ async def bookkeeping_document_update(
|
|
|
151
166
|
document = await document_by_number(slug, args.document_number)
|
|
152
167
|
body = await resolve_document_payload(slug, args.payload, is_patch=True)
|
|
153
168
|
path = f"/v1/business/{slug}/document/{document['id']}/"
|
|
154
|
-
await confirm_mutation(
|
|
155
|
-
business=slug,
|
|
156
|
-
tool_name="bookkeeping_document_update",
|
|
157
|
-
target_resource={
|
|
158
|
-
"type": "document",
|
|
159
|
-
"id": int(document["id"]),
|
|
160
|
-
},
|
|
161
|
-
parameters=body,
|
|
162
|
-
)
|
|
163
169
|
updated = await get_client().request(
|
|
164
170
|
"PATCH",
|
|
165
171
|
path,
|
|
@@ -179,6 +185,12 @@ async def bookkeeping_document_update(
|
|
|
179
185
|
|
|
180
186
|
@tool(
|
|
181
187
|
name="bookkeeping_document_delete",
|
|
188
|
+
annotations=ToolAnnotations(
|
|
189
|
+
readOnlyHint=False,
|
|
190
|
+
destructiveHint=True,
|
|
191
|
+
idempotentHint=False,
|
|
192
|
+
openWorldHint=False,
|
|
193
|
+
),
|
|
182
194
|
description="Delete a bookkeeping document selected by document_number.",
|
|
183
195
|
)
|
|
184
196
|
async def bookkeeping_document_delete(
|
|
@@ -188,14 +200,6 @@ async def bookkeeping_document_delete(
|
|
|
188
200
|
slug = await business_slug(args.business)
|
|
189
201
|
document = await document_by_number(slug, args.document_number)
|
|
190
202
|
path = f"/v1/business/{slug}/document/{document['id']}/"
|
|
191
|
-
await confirm_mutation(
|
|
192
|
-
business=slug,
|
|
193
|
-
tool_name="bookkeeping_document_delete",
|
|
194
|
-
target_resource={
|
|
195
|
-
"type": "document",
|
|
196
|
-
"id": int(document["id"]),
|
|
197
|
-
},
|
|
198
|
-
)
|
|
199
203
|
await get_client().request(
|
|
200
204
|
"DELETE",
|
|
201
205
|
path,
|
|
@@ -206,6 +210,12 @@ async def bookkeeping_document_delete(
|
|
|
206
210
|
|
|
207
211
|
@tool(
|
|
208
212
|
name="bookkeeping_entries_list",
|
|
213
|
+
annotations=ToolAnnotations(
|
|
214
|
+
readOnlyHint=True,
|
|
215
|
+
destructiveHint=False,
|
|
216
|
+
idempotentHint=True,
|
|
217
|
+
openWorldHint=False,
|
|
218
|
+
),
|
|
209
219
|
description="List realized journal entries for a document. Entries are generated from blueprint and are read-only in MCP.",
|
|
210
220
|
output_schema=ListEnvelope[EntrySummary].model_json_schema(),
|
|
211
221
|
)
|
|
@@ -227,6 +237,12 @@ async def bookkeeping_entries_list(
|
|
|
227
237
|
|
|
228
238
|
@tool(
|
|
229
239
|
name="bookkeeping_document_finalize_active_suggestion",
|
|
240
|
+
annotations=ToolAnnotations(
|
|
241
|
+
readOnlyHint=False,
|
|
242
|
+
destructiveHint=False,
|
|
243
|
+
idempotentHint=False,
|
|
244
|
+
openWorldHint=False,
|
|
245
|
+
),
|
|
230
246
|
description="Apply the active accounting suggestion for a draft document and finalize it.",
|
|
231
247
|
)
|
|
232
248
|
async def bookkeeping_document_finalize_active_suggestion(
|
|
@@ -239,14 +255,6 @@ async def bookkeeping_document_finalize_active_suggestion(
|
|
|
239
255
|
f"/v1/mcp/business/{slug}/documents/{document['id']}/"
|
|
240
256
|
"actions/finalize_active_suggestion/"
|
|
241
257
|
)
|
|
242
|
-
await confirm_mutation(
|
|
243
|
-
business=slug,
|
|
244
|
-
tool_name="bookkeeping_document_finalize_active_suggestion",
|
|
245
|
-
target_resource={
|
|
246
|
-
"type": "document",
|
|
247
|
-
"id": int(document["id"]),
|
|
248
|
-
},
|
|
249
|
-
)
|
|
250
258
|
result = await get_client().request(
|
|
251
259
|
"POST",
|
|
252
260
|
path,
|
|
@@ -258,6 +266,12 @@ async def bookkeeping_document_finalize_active_suggestion(
|
|
|
258
266
|
|
|
259
267
|
@tool(
|
|
260
268
|
name="bookkeeping_document_action",
|
|
269
|
+
annotations=ToolAnnotations(
|
|
270
|
+
readOnlyHint=False,
|
|
271
|
+
destructiveHint=False,
|
|
272
|
+
idempotentHint=False,
|
|
273
|
+
openWorldHint=False,
|
|
274
|
+
),
|
|
261
275
|
description="Run a state action on a document: lock, unlock, flag, or unflag.",
|
|
262
276
|
)
|
|
263
277
|
async def bookkeeping_document_action(
|
|
@@ -267,14 +281,6 @@ async def bookkeeping_document_action(
|
|
|
267
281
|
slug = await business_slug(args.business)
|
|
268
282
|
document = await document_by_number(slug, args.document_number)
|
|
269
283
|
path = f"/v1/business/{slug}/document/{document['id']}/action/{args.action.value}/"
|
|
270
|
-
await confirm_mutation(
|
|
271
|
-
business=slug,
|
|
272
|
-
tool_name="bookkeeping_document_action",
|
|
273
|
-
target_resource={
|
|
274
|
-
"type": "document",
|
|
275
|
-
"id": int(document["id"]),
|
|
276
|
-
},
|
|
277
|
-
)
|
|
278
284
|
result = await get_client().request(
|
|
279
285
|
"POST",
|
|
280
286
|
path,
|
|
@@ -5,9 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from fastmcp.tools import tool
|
|
8
|
+
from fastmcp.tools.tool import ToolAnnotations
|
|
8
9
|
from fastmcp.exceptions import ToolError
|
|
9
10
|
|
|
10
|
-
from nocfo_toolkit.mcp.curated.confirmation import confirm_mutation
|
|
11
11
|
from nocfo_toolkit.mcp.curated.runtime import business_slug, get_client
|
|
12
12
|
from nocfo_toolkit.mcp.curated.errors import raise_tool_error
|
|
13
13
|
from nocfo_toolkit.mcp.curated.schemas import (
|
|
@@ -25,6 +25,12 @@ header_fields = ("id", "name", "type", "parent_id", "parent_ids", "level")
|
|
|
25
25
|
|
|
26
26
|
@tool(
|
|
27
27
|
name="bookkeeping_headers_list",
|
|
28
|
+
annotations=ToolAnnotations(
|
|
29
|
+
readOnlyHint=True,
|
|
30
|
+
destructiveHint=False,
|
|
31
|
+
idempotentHint=True,
|
|
32
|
+
openWorldHint=False,
|
|
33
|
+
),
|
|
28
34
|
description="List optional account header hierarchy. Returns feature_disabled if headers are not enabled.",
|
|
29
35
|
output_schema=ListEnvelope[HeaderSummary].model_json_schema(),
|
|
30
36
|
)
|
|
@@ -55,6 +61,12 @@ async def bookkeeping_headers_list(params: HeaderListInput) -> dict[str, Any]:
|
|
|
55
61
|
|
|
56
62
|
@tool(
|
|
57
63
|
name="bookkeeping_header_retrieve",
|
|
64
|
+
annotations=ToolAnnotations(
|
|
65
|
+
readOnlyHint=True,
|
|
66
|
+
destructiveHint=False,
|
|
67
|
+
idempotentHint=True,
|
|
68
|
+
openWorldHint=False,
|
|
69
|
+
),
|
|
58
70
|
description="Retrieve one account header by header_id from bookkeeping_headers_list.",
|
|
59
71
|
)
|
|
60
72
|
async def bookkeeping_header_retrieve(params: HeaderIdInput) -> dict[str, Any]:
|
|
@@ -70,21 +82,18 @@ async def bookkeeping_header_retrieve(params: HeaderIdInput) -> dict[str, Any]:
|
|
|
70
82
|
|
|
71
83
|
@tool(
|
|
72
84
|
name="bookkeeping_header_create",
|
|
85
|
+
annotations=ToolAnnotations(
|
|
86
|
+
readOnlyHint=False,
|
|
87
|
+
destructiveHint=False,
|
|
88
|
+
idempotentHint=False,
|
|
89
|
+
openWorldHint=False,
|
|
90
|
+
),
|
|
73
91
|
description="Create an account header when account headers are enabled for the business.",
|
|
74
92
|
)
|
|
75
93
|
async def bookkeeping_header_create(params: HeaderPayloadInput) -> dict[str, Any]:
|
|
76
94
|
args = params
|
|
77
95
|
slug = await business_slug(args.business)
|
|
78
96
|
path = f"/v1/business/{slug}/header/"
|
|
79
|
-
await confirm_mutation(
|
|
80
|
-
business=slug,
|
|
81
|
-
tool_name="bookkeeping_header_create",
|
|
82
|
-
target_resource={
|
|
83
|
-
"type": "header",
|
|
84
|
-
"id": str(args.payload.get("name") or "new"),
|
|
85
|
-
},
|
|
86
|
-
parameters=args.payload,
|
|
87
|
-
)
|
|
88
97
|
result = await get_client().request(
|
|
89
98
|
"POST",
|
|
90
99
|
path,
|