jabb-cli 0.1.0__tar.gz → 0.2.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.
jabb_cli-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JABB (JAB-Bers)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: jabb-cli
3
+ Version: 0.2.0
4
+ Summary: CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment.
5
+ Author: JABB
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/JAB-Bers/jabb-cli
8
+ Project-URL: Repository, https://github.com/JAB-Bers/jabb-cli
9
+ Project-URL: Issues, https://github.com/JAB-Bers/jabb-cli/issues
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: click>=8.1
20
+ Requires-Dist: requests>=2.31
21
+ Requires-Dist: mcp<2,>=1.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # jabb
27
+
28
+ CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment. A thin wrapper: every command is one HTTP GET, no business logic.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install -e .
34
+ ```
35
+
36
+ ## Quickstart
37
+
38
+ ```bash
39
+ export JABB_API_KEY=jabb_test_...
40
+ jabb locations list
41
+ ```
42
+
43
+ Time to first result: under 5 minutes from a minted key.
44
+
45
+ ## Usage
46
+
47
+ ```
48
+ jabb [--json] [--detailed] <resource> list [--cursor <opaque>] [--limit <1-100>]
49
+ ```
50
+
51
+ Resources: `locations`, `scores`, `reviews`, `pnif`, `sentiment`.
52
+
53
+ - `--json` — print the raw API envelope (`{"data": [...], "next_cursor": ...}`) instead of a table. Use for scripting/agents.
54
+ - `--detailed` — request more fields per row (costs more tokens/bandwidth).
55
+ - `--cursor` — pass the previous response's `next_cursor` to page forward.
56
+ - `--limit` — rows per page, 1-100, default 20.
57
+
58
+ Examples:
59
+
60
+ ```bash
61
+ jabb scores list --detailed
62
+ jabb reviews list --limit 5
63
+ jabb pnif list --json
64
+ jabb locations list --cursor eyJwIjo... --limit 50
65
+ ```
66
+
67
+ ## Auth
68
+
69
+ Set `JABB_API_KEY` to a key minted by your JABB admin (`jabb_live_...` in production, `jabb_test_...` for sandbox). No config file, no `--api-key` flag — env var only, so keys don't land in shell history.
70
+
71
+ For local development against a jabb-server instance running on your machine:
72
+
73
+ ```bash
74
+ export JABB_API_BASE_URL=http://127.0.0.1:5500
75
+ ```
76
+
77
+ Default base URL is `https://api.jabb.cx`. There is no automatic failover to `api.jabb.pro` — that host runs a separate, diverged database, and silently reading from it would risk returning stale data.
78
+
79
+ ## Exit codes
80
+
81
+ | Code | Meaning |
82
+ |---|---|
83
+ | 0 | Success |
84
+ | 1 | Generic API error |
85
+ | 2 | Auth/config error (missing/invalid/revoked key, wrong scope) |
86
+ | 3 | Rate limited (message includes retry-after seconds) |
87
+
88
+ ## MCP server (agent integration)
89
+
90
+ `jabb mcp` starts a **stdio MCP server** that exposes the same 5 resources as agent tools, so any MCP host (Claude Code, Claude Desktop, Cursor…) can query your JABB data with nothing but an API key.
91
+
92
+ ### Tools exposed
93
+
94
+ | Tool | When to call |
95
+ |------|-------------|
96
+ | `jabb_list_locations` | User asks about branches/stores or needs location IDs |
97
+ | `jabb_get_cx_scores` | User wants quantitative CX quality metrics |
98
+ | `jabb_list_quick_jabbs` | User wants individual evaluation content / verbatim feedback |
99
+ | `jabb_get_pnif` | User asks for Price/Need/Impact/Frequency breakdown |
100
+ | `jabb_get_sentiment` | User asks for sentiment labels or topic-level trends |
101
+
102
+ All tools accept `cursor`, `limit`, `response_format` (`"concise"` | `"detailed"`). `jabb_get_pnif` and `jabb_get_sentiment` also accept `language` (ISO 639-1: `en`/`fr`/`ar`).
103
+
104
+ Responses are capped at ~25 KB. When truncated, the payload includes `"truncated": true` and preserves the original `next_cursor` so the agent can page forward.
105
+
106
+ API errors are returned as tool errors carrying the API's `code` + `hint` verbatim. 429 errors also include `Retry-After` seconds.
107
+
108
+ ### Connect from Claude Code
109
+
110
+ ```bash
111
+ # One-time setup (project scope):
112
+ claude mcp add jabb -- jabb mcp
113
+ ```
114
+
115
+ Or add a project-scope `.mcp.json` at your repo root:
116
+
117
+ ```json
118
+ {
119
+ "mcpServers": {
120
+ "jabb": {
121
+ "command": "jabb",
122
+ "args": ["mcp"],
123
+ "env": {
124
+ "JABB_API_KEY": "${JABB_API_KEY}"
125
+ }
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ ### Claude Desktop (`claude_desktop_config.json`)
132
+
133
+ ```json
134
+ {
135
+ "mcpServers": {
136
+ "jabb": {
137
+ "command": "jabb",
138
+ "args": ["mcp"],
139
+ "env": {
140
+ "JABB_API_KEY": "jabb_live_..."
141
+ }
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ ### Environment variables
148
+
149
+ | Variable | Required | Default | Description |
150
+ |----------|----------|---------|-------------|
151
+ | `JABB_API_KEY` | Yes | — | API key minted by your JABB admin |
152
+ | `JABB_API_BASE_URL` | No | `https://api.jabb.cx` | Override for local dev |
153
+
154
+ ## For Claude Code users
155
+
156
+ See `skills/jabb-cx/SKILL.md` — copy it into your project's `.claude/skills/jabb-cx/` to let Claude Code drive this CLI directly when you ask about your CX data. The MCP server (`jabb mcp`) is an alternative path that works with any MCP-capable host without the skill.
157
+
158
+ ## Scope
159
+
160
+ Read-only. `list` operations against 5 resources, nothing else. No write/create/update commands exist.
@@ -0,0 +1,135 @@
1
+ # jabb
2
+
3
+ CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment. A thin wrapper: every command is one HTTP GET, no business logic.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install -e .
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```bash
14
+ export JABB_API_KEY=jabb_test_...
15
+ jabb locations list
16
+ ```
17
+
18
+ Time to first result: under 5 minutes from a minted key.
19
+
20
+ ## Usage
21
+
22
+ ```
23
+ jabb [--json] [--detailed] <resource> list [--cursor <opaque>] [--limit <1-100>]
24
+ ```
25
+
26
+ Resources: `locations`, `scores`, `reviews`, `pnif`, `sentiment`.
27
+
28
+ - `--json` — print the raw API envelope (`{"data": [...], "next_cursor": ...}`) instead of a table. Use for scripting/agents.
29
+ - `--detailed` — request more fields per row (costs more tokens/bandwidth).
30
+ - `--cursor` — pass the previous response's `next_cursor` to page forward.
31
+ - `--limit` — rows per page, 1-100, default 20.
32
+
33
+ Examples:
34
+
35
+ ```bash
36
+ jabb scores list --detailed
37
+ jabb reviews list --limit 5
38
+ jabb pnif list --json
39
+ jabb locations list --cursor eyJwIjo... --limit 50
40
+ ```
41
+
42
+ ## Auth
43
+
44
+ Set `JABB_API_KEY` to a key minted by your JABB admin (`jabb_live_...` in production, `jabb_test_...` for sandbox). No config file, no `--api-key` flag — env var only, so keys don't land in shell history.
45
+
46
+ For local development against a jabb-server instance running on your machine:
47
+
48
+ ```bash
49
+ export JABB_API_BASE_URL=http://127.0.0.1:5500
50
+ ```
51
+
52
+ Default base URL is `https://api.jabb.cx`. There is no automatic failover to `api.jabb.pro` — that host runs a separate, diverged database, and silently reading from it would risk returning stale data.
53
+
54
+ ## Exit codes
55
+
56
+ | Code | Meaning |
57
+ |---|---|
58
+ | 0 | Success |
59
+ | 1 | Generic API error |
60
+ | 2 | Auth/config error (missing/invalid/revoked key, wrong scope) |
61
+ | 3 | Rate limited (message includes retry-after seconds) |
62
+
63
+ ## MCP server (agent integration)
64
+
65
+ `jabb mcp` starts a **stdio MCP server** that exposes the same 5 resources as agent tools, so any MCP host (Claude Code, Claude Desktop, Cursor…) can query your JABB data with nothing but an API key.
66
+
67
+ ### Tools exposed
68
+
69
+ | Tool | When to call |
70
+ |------|-------------|
71
+ | `jabb_list_locations` | User asks about branches/stores or needs location IDs |
72
+ | `jabb_get_cx_scores` | User wants quantitative CX quality metrics |
73
+ | `jabb_list_quick_jabbs` | User wants individual evaluation content / verbatim feedback |
74
+ | `jabb_get_pnif` | User asks for Price/Need/Impact/Frequency breakdown |
75
+ | `jabb_get_sentiment` | User asks for sentiment labels or topic-level trends |
76
+
77
+ All tools accept `cursor`, `limit`, `response_format` (`"concise"` | `"detailed"`). `jabb_get_pnif` and `jabb_get_sentiment` also accept `language` (ISO 639-1: `en`/`fr`/`ar`).
78
+
79
+ Responses are capped at ~25 KB. When truncated, the payload includes `"truncated": true` and preserves the original `next_cursor` so the agent can page forward.
80
+
81
+ API errors are returned as tool errors carrying the API's `code` + `hint` verbatim. 429 errors also include `Retry-After` seconds.
82
+
83
+ ### Connect from Claude Code
84
+
85
+ ```bash
86
+ # One-time setup (project scope):
87
+ claude mcp add jabb -- jabb mcp
88
+ ```
89
+
90
+ Or add a project-scope `.mcp.json` at your repo root:
91
+
92
+ ```json
93
+ {
94
+ "mcpServers": {
95
+ "jabb": {
96
+ "command": "jabb",
97
+ "args": ["mcp"],
98
+ "env": {
99
+ "JABB_API_KEY": "${JABB_API_KEY}"
100
+ }
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ ### Claude Desktop (`claude_desktop_config.json`)
107
+
108
+ ```json
109
+ {
110
+ "mcpServers": {
111
+ "jabb": {
112
+ "command": "jabb",
113
+ "args": ["mcp"],
114
+ "env": {
115
+ "JABB_API_KEY": "jabb_live_..."
116
+ }
117
+ }
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### Environment variables
123
+
124
+ | Variable | Required | Default | Description |
125
+ |----------|----------|---------|-------------|
126
+ | `JABB_API_KEY` | Yes | — | API key minted by your JABB admin |
127
+ | `JABB_API_BASE_URL` | No | `https://api.jabb.cx` | Override for local dev |
128
+
129
+ ## For Claude Code users
130
+
131
+ See `skills/jabb-cx/SKILL.md` — copy it into your project's `.claude/skills/jabb-cx/` to let Claude Code drive this CLI directly when you ask about your CX data. The MCP server (`jabb mcp`) is an alternative path that works with any MCP-capable host without the skill.
132
+
133
+ ## Scope
134
+
135
+ Read-only. `list` operations against 5 resources, nothing else. No write/create/update commands exist.
@@ -75,3 +75,9 @@ def _make_group(name, path, help_text):
75
75
 
76
76
  for _name, (_path, _help) in RESOURCES.items():
77
77
  main.add_command(_make_group(_name, _path, _help))
78
+
79
+
80
+ @main.command(name="mcp", help="Start a stdio MCP server exposing JABB Public API v1 as agent tools.")
81
+ def mcp_serve():
82
+ from jabb_cli.mcp_server import serve
83
+ serve()
@@ -0,0 +1,156 @@
1
+ """stdio MCP server — exposes JABB Public API v1 as agent tools via FastMCP."""
2
+ import json
3
+ import sys
4
+ from typing import Literal, Optional
5
+
6
+ from mcp.server.fastmcp import FastMCP
7
+ from jabb_cli.client import ApiClient, ApiError, AuthError, RateLimitError
8
+ from jabb_cli.cli import RESOURCES
9
+
10
+ _MAX_RESPONSE_BYTES = 25_000 # ~25K tokens guard
11
+
12
+ mcp = FastMCP("jabb")
13
+
14
+
15
+ def _client() -> ApiClient:
16
+ """Instantiate ApiClient; raises AuthError on missing key (propagated as tool error)."""
17
+ return ApiClient()
18
+
19
+
20
+ def _call(path: str, params: dict) -> dict:
21
+ return _client().get(path, params=params)
22
+
23
+
24
+ def _truncate(payload: dict) -> dict:
25
+ """Cap payload at _MAX_RESPONSE_BYTES. Sets truncated+next_cursor when trimmed."""
26
+ raw = json.dumps(payload)
27
+ if len(raw.encode()) <= _MAX_RESPONSE_BYTES:
28
+ return payload
29
+ data = payload.get("data", [])
30
+ if isinstance(data, list):
31
+ # Binary-search for the largest prefix that fits
32
+ lo, hi = 0, len(data)
33
+ while lo < hi:
34
+ mid = (lo + hi + 1) // 2
35
+ candidate = dict(payload, data=data[:mid], truncated=True)
36
+ if len(json.dumps(candidate).encode()) <= _MAX_RESPONSE_BYTES:
37
+ lo = mid
38
+ else:
39
+ hi = mid - 1
40
+ return dict(payload, data=data[:lo], truncated=True)
41
+ # Non-list data: return as-is with truncation flag (can't safely trim objects)
42
+ return dict(payload, truncated=True)
43
+
44
+
45
+ def _tool_error(e: ApiError) -> str:
46
+ parts = [f"[{e.code}] {e.message}"]
47
+ if e.hint:
48
+ parts.append(f"Hint: {e.hint}")
49
+ if isinstance(e, RateLimitError):
50
+ parts.append(f"Retry-After: {e.retry_after}s")
51
+ return " | ".join(parts)
52
+
53
+
54
+ def _list_params(
55
+ cursor: Optional[str],
56
+ limit: int,
57
+ response_format: Literal["concise", "detailed"],
58
+ extra: Optional[dict] = None,
59
+ ) -> dict:
60
+ p: dict = {"limit": limit, "response_format": response_format}
61
+ if cursor:
62
+ p["cursor"] = cursor
63
+ if extra:
64
+ p.update(extra)
65
+ return p
66
+
67
+
68
+ @mcp.tool()
69
+ def jabb_list_locations(
70
+ cursor: Optional[str] = None,
71
+ limit: int = 20,
72
+ response_format: Literal["concise", "detailed"] = "concise",
73
+ ) -> dict:
74
+ """Use when the user asks about physical branches/stores of the business, needs a
75
+ store/venue list, or requires location IDs to cross-reference other resources.
76
+ Returns paginated list of business locations. Use concise unless field depth is needed."""
77
+ try:
78
+ result = _call(RESOURCES["locations"][0], _list_params(cursor, limit, response_format))
79
+ return _truncate(result)
80
+ except ApiError as e:
81
+ raise ValueError(_tool_error(e)) from e
82
+
83
+
84
+ @mcp.tool()
85
+ def jabb_get_cx_scores(
86
+ cursor: Optional[str] = None,
87
+ limit: int = 20,
88
+ response_format: Literal["concise", "detailed"] = "concise",
89
+ ) -> dict:
90
+ """Use when the user asks for quantitative CX quality metrics, scores, or ratings per
91
+ entity. Do NOT use for raw review text — use jabb_list_quick_jabbs instead."""
92
+ try:
93
+ result = _call(RESOURCES["scores"][0], _list_params(cursor, limit, response_format))
94
+ return _truncate(result)
95
+ except ApiError as e:
96
+ raise ValueError(_tool_error(e)) from e
97
+
98
+
99
+ @mcp.tool()
100
+ def jabb_list_quick_jabbs(
101
+ cursor: Optional[str] = None,
102
+ limit: int = 20,
103
+ response_format: Literal["concise", "detailed"] = "concise",
104
+ ) -> dict:
105
+ """Use when the user asks about individual customer evaluations, review content, or
106
+ specific ratings with verbatim feedback. Do NOT use for aggregate scores — use
107
+ jabb_get_cx_scores instead."""
108
+ try:
109
+ result = _call(RESOURCES["reviews"][0], _list_params(cursor, limit, response_format))
110
+ return _truncate(result)
111
+ except ApiError as e:
112
+ raise ValueError(_tool_error(e)) from e
113
+
114
+
115
+ @mcp.tool()
116
+ def jabb_get_pnif(
117
+ language: str = "en",
118
+ cursor: Optional[str] = None,
119
+ limit: int = 20,
120
+ response_format: Literal["concise", "detailed"] = "concise",
121
+ ) -> dict:
122
+ """Use when the user asks for the Price/Need/Impact/Frequency analysis breakdown.
123
+ Do NOT use for raw scores or review text. language: ISO 639-1 code (en/fr/ar)."""
124
+ try:
125
+ extra = {"language": language}
126
+ result = _call(RESOURCES["pnif"][0], _list_params(cursor, limit, response_format, extra))
127
+ return _truncate(result)
128
+ except ApiError as e:
129
+ raise ValueError(_tool_error(e)) from e
130
+
131
+
132
+ @mcp.tool()
133
+ def jabb_get_sentiment(
134
+ language: str = "en",
135
+ cursor: Optional[str] = None,
136
+ limit: int = 20,
137
+ response_format: Literal["concise", "detailed"] = "concise",
138
+ ) -> dict:
139
+ """Use when the user asks for sentiment labels, trends, or topic-level sentiment mapping.
140
+ Do NOT use for raw scores or PNIF breakdown. language: ISO 639-1 code (en/fr/ar)."""
141
+ try:
142
+ extra = {"language": language}
143
+ result = _call(RESOURCES["sentiment"][0], _list_params(cursor, limit, response_format, extra))
144
+ return _truncate(result)
145
+ except ApiError as e:
146
+ raise ValueError(_tool_error(e)) from e
147
+
148
+
149
+ def serve() -> None:
150
+ """Entry point: validate key then run stdio MCP server."""
151
+ try:
152
+ ApiClient() # fail-fast: raises AuthError if JABB_API_KEY missing
153
+ except AuthError as e:
154
+ print(f"[jabb-mcp] {e.message} {e.hint}", file=sys.stderr)
155
+ sys.exit(2)
156
+ mcp.run(transport="stdio")
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: jabb-cli
3
+ Version: 0.2.0
4
+ Summary: CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment.
5
+ Author: JABB
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/JAB-Bers/jabb-cli
8
+ Project-URL: Repository, https://github.com/JAB-Bers/jabb-cli
9
+ Project-URL: Issues, https://github.com/JAB-Bers/jabb-cli/issues
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: click>=8.1
20
+ Requires-Dist: requests>=2.31
21
+ Requires-Dist: mcp<2,>=1.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # jabb
27
+
28
+ CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment. A thin wrapper: every command is one HTTP GET, no business logic.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install -e .
34
+ ```
35
+
36
+ ## Quickstart
37
+
38
+ ```bash
39
+ export JABB_API_KEY=jabb_test_...
40
+ jabb locations list
41
+ ```
42
+
43
+ Time to first result: under 5 minutes from a minted key.
44
+
45
+ ## Usage
46
+
47
+ ```
48
+ jabb [--json] [--detailed] <resource> list [--cursor <opaque>] [--limit <1-100>]
49
+ ```
50
+
51
+ Resources: `locations`, `scores`, `reviews`, `pnif`, `sentiment`.
52
+
53
+ - `--json` — print the raw API envelope (`{"data": [...], "next_cursor": ...}`) instead of a table. Use for scripting/agents.
54
+ - `--detailed` — request more fields per row (costs more tokens/bandwidth).
55
+ - `--cursor` — pass the previous response's `next_cursor` to page forward.
56
+ - `--limit` — rows per page, 1-100, default 20.
57
+
58
+ Examples:
59
+
60
+ ```bash
61
+ jabb scores list --detailed
62
+ jabb reviews list --limit 5
63
+ jabb pnif list --json
64
+ jabb locations list --cursor eyJwIjo... --limit 50
65
+ ```
66
+
67
+ ## Auth
68
+
69
+ Set `JABB_API_KEY` to a key minted by your JABB admin (`jabb_live_...` in production, `jabb_test_...` for sandbox). No config file, no `--api-key` flag — env var only, so keys don't land in shell history.
70
+
71
+ For local development against a jabb-server instance running on your machine:
72
+
73
+ ```bash
74
+ export JABB_API_BASE_URL=http://127.0.0.1:5500
75
+ ```
76
+
77
+ Default base URL is `https://api.jabb.cx`. There is no automatic failover to `api.jabb.pro` — that host runs a separate, diverged database, and silently reading from it would risk returning stale data.
78
+
79
+ ## Exit codes
80
+
81
+ | Code | Meaning |
82
+ |---|---|
83
+ | 0 | Success |
84
+ | 1 | Generic API error |
85
+ | 2 | Auth/config error (missing/invalid/revoked key, wrong scope) |
86
+ | 3 | Rate limited (message includes retry-after seconds) |
87
+
88
+ ## MCP server (agent integration)
89
+
90
+ `jabb mcp` starts a **stdio MCP server** that exposes the same 5 resources as agent tools, so any MCP host (Claude Code, Claude Desktop, Cursor…) can query your JABB data with nothing but an API key.
91
+
92
+ ### Tools exposed
93
+
94
+ | Tool | When to call |
95
+ |------|-------------|
96
+ | `jabb_list_locations` | User asks about branches/stores or needs location IDs |
97
+ | `jabb_get_cx_scores` | User wants quantitative CX quality metrics |
98
+ | `jabb_list_quick_jabbs` | User wants individual evaluation content / verbatim feedback |
99
+ | `jabb_get_pnif` | User asks for Price/Need/Impact/Frequency breakdown |
100
+ | `jabb_get_sentiment` | User asks for sentiment labels or topic-level trends |
101
+
102
+ All tools accept `cursor`, `limit`, `response_format` (`"concise"` | `"detailed"`). `jabb_get_pnif` and `jabb_get_sentiment` also accept `language` (ISO 639-1: `en`/`fr`/`ar`).
103
+
104
+ Responses are capped at ~25 KB. When truncated, the payload includes `"truncated": true` and preserves the original `next_cursor` so the agent can page forward.
105
+
106
+ API errors are returned as tool errors carrying the API's `code` + `hint` verbatim. 429 errors also include `Retry-After` seconds.
107
+
108
+ ### Connect from Claude Code
109
+
110
+ ```bash
111
+ # One-time setup (project scope):
112
+ claude mcp add jabb -- jabb mcp
113
+ ```
114
+
115
+ Or add a project-scope `.mcp.json` at your repo root:
116
+
117
+ ```json
118
+ {
119
+ "mcpServers": {
120
+ "jabb": {
121
+ "command": "jabb",
122
+ "args": ["mcp"],
123
+ "env": {
124
+ "JABB_API_KEY": "${JABB_API_KEY}"
125
+ }
126
+ }
127
+ }
128
+ }
129
+ ```
130
+
131
+ ### Claude Desktop (`claude_desktop_config.json`)
132
+
133
+ ```json
134
+ {
135
+ "mcpServers": {
136
+ "jabb": {
137
+ "command": "jabb",
138
+ "args": ["mcp"],
139
+ "env": {
140
+ "JABB_API_KEY": "jabb_live_..."
141
+ }
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ ### Environment variables
148
+
149
+ | Variable | Required | Default | Description |
150
+ |----------|----------|---------|-------------|
151
+ | `JABB_API_KEY` | Yes | — | API key minted by your JABB admin |
152
+ | `JABB_API_BASE_URL` | No | `https://api.jabb.cx` | Override for local dev |
153
+
154
+ ## For Claude Code users
155
+
156
+ See `skills/jabb-cx/SKILL.md` — copy it into your project's `.claude/skills/jabb-cx/` to let Claude Code drive this CLI directly when you ask about your CX data. The MCP server (`jabb mcp`) is an alternative path that works with any MCP-capable host without the skill.
157
+
158
+ ## Scope
159
+
160
+ Read-only. `list` operations against 5 resources, nothing else. No write/create/update commands exist.
@@ -1,8 +1,10 @@
1
+ LICENSE
1
2
  README.md
2
3
  pyproject.toml
3
4
  jabb_cli/__init__.py
4
5
  jabb_cli/cli.py
5
6
  jabb_cli/client.py
7
+ jabb_cli/mcp_server.py
6
8
  jabb_cli/output.py
7
9
  jabb_cli.egg-info/PKG-INFO
8
10
  jabb_cli.egg-info/SOURCES.txt
@@ -13,4 +15,6 @@ jabb_cli.egg-info/top_level.txt
13
15
  tests/test_cli_errors.py
14
16
  tests/test_cli_resources.py
15
17
  tests/test_client.py
18
+ tests/test_mcp_entry.py
19
+ tests/test_mcp_tools.py
16
20
  tests/test_output.py
@@ -1,5 +1,6 @@
1
1
  click>=8.1
2
2
  requests>=2.31
3
+ mcp<2,>=1.0
3
4
 
4
5
  [dev]
5
6
  pytest>=8.0
@@ -0,0 +1,35 @@
1
+ [project]
2
+ name = "jabb-cli"
3
+ version = "0.2.0"
4
+ description = "CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment."
5
+ requires-python = ">=3.11"
6
+ license = "MIT"
7
+ readme = "README.md"
8
+ authors = [{ name = "JABB" }]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Environment :: Console",
12
+ "Intended Audience :: Developers",
13
+ "Programming Language :: Python :: 3.11",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ ]
17
+ dependencies = ["click>=8.1", "requests>=2.31", "mcp>=1.0,<2"]
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/JAB-Bers/jabb-cli"
21
+ Repository = "https://github.com/JAB-Bers/jabb-cli"
22
+ Issues = "https://github.com/JAB-Bers/jabb-cli/issues"
23
+
24
+ [project.optional-dependencies]
25
+ dev = ["pytest>=8.0"]
26
+
27
+ [project.scripts]
28
+ jabb = "jabb_cli.cli:main"
29
+
30
+ [tool.setuptools.packages.find]
31
+ include = ["jabb_cli*"]
32
+
33
+ [build-system]
34
+ requires = ["setuptools>=68"]
35
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,28 @@
1
+ """Task 1 tests: mcp subcommand wired + missing-key fail-fast."""
2
+ import pytest
3
+ from unittest.mock import patch
4
+ from click.testing import CliRunner
5
+ from jabb_cli.cli import main
6
+
7
+
8
+ def test_mcp_subcommand_registered():
9
+ """jabb mcp --help must exit 0 (subcommand is registered)."""
10
+ result = CliRunner().invoke(main, ["mcp", "--help"])
11
+ assert result.exit_code == 0
12
+ assert "stdio" in result.output.lower() or "mcp" in result.output.lower()
13
+
14
+
15
+ def test_mcp_missing_key_exits_nonzero(monkeypatch):
16
+ """jabb mcp without JABB_API_KEY must exit 2 and print a message to stderr."""
17
+ monkeypatch.delenv("JABB_API_KEY", raising=False)
18
+ with patch("jabb_cli.mcp_server.mcp.run"): # prevent actual stdio server start
19
+ result = CliRunner().invoke(main, ["mcp"])
20
+ assert result.exit_code == 2
21
+
22
+
23
+ def test_mcp_with_key_calls_serve(monkeypatch):
24
+ """jabb mcp with a valid key proceeds to mcp.run (no early exit)."""
25
+ monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
26
+ with patch("jabb_cli.mcp_server.mcp.run") as mock_run:
27
+ CliRunner().invoke(main, ["mcp"])
28
+ mock_run.assert_called_once_with(transport="stdio")
@@ -0,0 +1,134 @@
1
+ """Task 2 tests: MCP tool path mapping, param forwarding, concise/detailed, error mapping, truncation."""
2
+ import json
3
+ import pytest
4
+ from unittest.mock import patch, MagicMock
5
+ from jabb_cli.mcp_server import (
6
+ jabb_list_locations,
7
+ jabb_get_cx_scores,
8
+ jabb_list_quick_jabbs,
9
+ jabb_get_pnif,
10
+ jabb_get_sentiment,
11
+ _truncate,
12
+ _MAX_RESPONSE_BYTES,
13
+ )
14
+ from jabb_cli.client import ApiError, AuthError, RateLimitError
15
+ from jabb_cli.cli import RESOURCES
16
+
17
+
18
+ # ── path mapping ──────────────────────────────────────────────────────────────
19
+
20
+ TOOL_PATH_CASES = [
21
+ (jabb_list_locations, "locations"),
22
+ (jabb_get_cx_scores, "scores"),
23
+ (jabb_list_quick_jabbs, "reviews"),
24
+ (jabb_get_pnif, "pnif"),
25
+ (jabb_get_sentiment, "sentiment"),
26
+ ]
27
+
28
+
29
+ @pytest.mark.parametrize("tool_fn,resource_key", TOOL_PATH_CASES)
30
+ def test_tool_calls_correct_path(monkeypatch, tool_fn, resource_key):
31
+ monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
32
+ expected_path = RESOURCES[resource_key][0]
33
+ with patch("jabb_cli.mcp_server.ApiClient.get", return_value={"data": [], "next_cursor": None}) as m:
34
+ tool_fn()
35
+ assert m.call_args.args[0] == expected_path
36
+
37
+
38
+ # ── param forwarding ──────────────────────────────────────────────────────────
39
+
40
+ def test_cursor_and_limit_forwarded(monkeypatch):
41
+ monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
42
+ with patch("jabb_cli.mcp_server.ApiClient.get", return_value={"data": [], "next_cursor": None}) as m:
43
+ jabb_list_locations(cursor="tok_abc", limit=5)
44
+ params = m.call_args.kwargs["params"]
45
+ assert params["cursor"] == "tok_abc"
46
+ assert params["limit"] == 5
47
+
48
+
49
+ def test_language_forwarded_pnif(monkeypatch):
50
+ monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
51
+ with patch("jabb_cli.mcp_server.ApiClient.get", return_value={"data": {}, "next_cursor": None}) as m:
52
+ jabb_get_pnif(language="ar")
53
+ assert m.call_args.kwargs["params"]["language"] == "ar"
54
+
55
+
56
+ def test_language_forwarded_sentiment(monkeypatch):
57
+ monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
58
+ with patch("jabb_cli.mcp_server.ApiClient.get", return_value={"data": {}, "next_cursor": None}) as m:
59
+ jabb_get_sentiment(language="fr")
60
+ assert m.call_args.kwargs["params"]["language"] == "fr"
61
+
62
+
63
+ # ── concise vs detailed ───────────────────────────────────────────────────────
64
+
65
+ @pytest.mark.parametrize("fmt", ["concise", "detailed"])
66
+ def test_response_format_forwarded(monkeypatch, fmt):
67
+ monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
68
+ with patch("jabb_cli.mcp_server.ApiClient.get", return_value={"data": [], "next_cursor": None}) as m:
69
+ jabb_list_locations(response_format=fmt)
70
+ assert m.call_args.kwargs["params"]["response_format"] == fmt
71
+
72
+
73
+ # ── error mapping ─────────────────────────────────────────────────────────────
74
+
75
+ def test_api_error_raises_value_error_with_hint(monkeypatch):
76
+ monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
77
+ with patch("jabb_cli.mcp_server.ApiClient.get", side_effect=ApiError("not_found", "Resource not found", "Check the entity ID.")):
78
+ with pytest.raises(ValueError) as exc:
79
+ jabb_list_locations()
80
+ assert "not_found" in str(exc.value)
81
+ assert "Check the entity ID." in str(exc.value)
82
+
83
+
84
+ def test_rate_limit_error_includes_retry_after(monkeypatch):
85
+ monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
86
+ with patch("jabb_cli.mcp_server.ApiClient.get", side_effect=RateLimitError("rate_limited", "Too many requests", "Slow down.", 42)):
87
+ with pytest.raises(ValueError) as exc:
88
+ jabb_list_locations()
89
+ assert "42" in str(exc.value)
90
+ assert "Retry-After" in str(exc.value)
91
+
92
+
93
+ def test_auth_error_propagated(monkeypatch):
94
+ monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
95
+ with patch("jabb_cli.mcp_server.ApiClient.get", side_effect=AuthError("forbidden", "Invalid key.", "Regenerate key in JABB admin.")):
96
+ with pytest.raises(ValueError) as exc:
97
+ jabb_list_locations()
98
+ assert "forbidden" in str(exc.value)
99
+
100
+
101
+ # ── 25K truncation ────────────────────────────────────────────────────────────
102
+
103
+ def test_truncate_small_payload_unchanged():
104
+ payload = {"data": [{"id": "1"}], "next_cursor": None}
105
+ assert _truncate(payload) == payload
106
+
107
+
108
+ def test_truncate_large_list_sets_truncated_flag():
109
+ big_item = {"id": "x", "description": "a" * 500}
110
+ items = [big_item] * 200 # well over 25K
111
+ payload = {"data": items, "next_cursor": "tok_next"}
112
+ result = _truncate(payload)
113
+ assert result["truncated"] is True
114
+ assert len(result["data"]) < 200
115
+ assert len(json.dumps(result).encode()) <= _MAX_RESPONSE_BYTES
116
+
117
+
118
+ def test_truncate_sets_truncated_and_next_cursor_survives(monkeypatch):
119
+ monkeypatch.setenv("JABB_API_KEY", "jabb_test_x")
120
+ big_item = {"id": "x", "description": "a" * 500}
121
+ items = [big_item] * 200
122
+ large_response = {"data": items, "next_cursor": "tok_next"}
123
+ with patch("jabb_cli.mcp_server.ApiClient.get", return_value=large_response):
124
+ result = jabb_list_locations()
125
+ assert result["truncated"] is True
126
+ assert result["next_cursor"] == "tok_next"
127
+
128
+
129
+ def test_truncate_non_list_data_flag_only():
130
+ big_obj = {"key": "v" * 30_000}
131
+ payload = {"data": big_obj, "next_cursor": None}
132
+ result = _truncate(payload)
133
+ assert result["truncated"] is True
134
+ assert result["data"] == big_obj # object preserved, can't trim safely
jabb_cli-0.1.0/PKG-INFO DELETED
@@ -1,9 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: jabb-cli
3
- Version: 0.1.0
4
- Summary: CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment.
5
- Requires-Python: >=3.11
6
- Requires-Dist: click>=8.1
7
- Requires-Dist: requests>=2.31
8
- Provides-Extra: dev
9
- Requires-Dist: pytest>=8.0; extra == "dev"
jabb_cli-0.1.0/README.md DELETED
@@ -1,69 +0,0 @@
1
- # jabb
2
-
3
- CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment. A thin wrapper: every command is one HTTP GET, no business logic.
4
-
5
- ## Install
6
-
7
- ```bash
8
- pip install -e .
9
- ```
10
-
11
- ## Quickstart
12
-
13
- ```bash
14
- export JABB_API_KEY=jabb_test_...
15
- jabb locations list
16
- ```
17
-
18
- Time to first result: under 5 minutes from a minted key.
19
-
20
- ## Usage
21
-
22
- ```
23
- jabb [--json] [--detailed] <resource> list [--cursor <opaque>] [--limit <1-100>]
24
- ```
25
-
26
- Resources: `locations`, `scores`, `reviews`, `pnif`, `sentiment`.
27
-
28
- - `--json` — print the raw API envelope (`{"data": [...], "next_cursor": ...}`) instead of a table. Use for scripting/agents.
29
- - `--detailed` — request more fields per row (costs more tokens/bandwidth).
30
- - `--cursor` — pass the previous response's `next_cursor` to page forward.
31
- - `--limit` — rows per page, 1-100, default 20.
32
-
33
- Examples:
34
-
35
- ```bash
36
- jabb scores list --detailed
37
- jabb reviews list --limit 5
38
- jabb pnif list --json
39
- jabb locations list --cursor eyJwIjo... --limit 50
40
- ```
41
-
42
- ## Auth
43
-
44
- Set `JABB_API_KEY` to a key minted by your JABB admin (`jabb_live_...` in production, `jabb_test_...` for sandbox). No config file, no `--api-key` flag — env var only, so keys don't land in shell history.
45
-
46
- For local development against a jabb-server instance running on your machine:
47
-
48
- ```bash
49
- export JABB_API_BASE_URL=http://127.0.0.1:5500
50
- ```
51
-
52
- Default base URL is `https://api.jabb.cx`. There is no automatic failover to `api.jabb.pro` — that host runs a separate, diverged database, and silently reading from it would risk returning stale data.
53
-
54
- ## Exit codes
55
-
56
- | Code | Meaning |
57
- |---|---|
58
- | 0 | Success |
59
- | 1 | Generic API error |
60
- | 2 | Auth/config error (missing/invalid/revoked key, wrong scope) |
61
- | 3 | Rate limited (message includes retry-after seconds) |
62
-
63
- ## For Claude Code users
64
-
65
- See `skills/jabb-cx/SKILL.md` — copy it into your project's `.claude/skills/jabb-cx/` to let Claude Code drive this CLI directly when you ask about your CX data.
66
-
67
- ## Scope
68
-
69
- Read-only. `list` operations against 5 resources, nothing else. No write/create/update commands exist.
@@ -1,9 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: jabb-cli
3
- Version: 0.1.0
4
- Summary: CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment.
5
- Requires-Python: >=3.11
6
- Requires-Dist: click>=8.1
7
- Requires-Dist: requests>=2.31
8
- Provides-Extra: dev
9
- Requires-Dist: pytest>=8.0; extra == "dev"
@@ -1,19 +0,0 @@
1
- [project]
2
- name = "jabb-cli"
3
- version = "0.1.0"
4
- description = "CLI for JABB Public API v1 — locations, CX scores, reviews, PNIF, sentiment."
5
- requires-python = ">=3.11"
6
- dependencies = ["click>=8.1", "requests>=2.31"]
7
-
8
- [project.optional-dependencies]
9
- dev = ["pytest>=8.0"]
10
-
11
- [project.scripts]
12
- jabb = "jabb_cli.cli:main"
13
-
14
- [tool.setuptools.packages.find]
15
- include = ["jabb_cli*"]
16
-
17
- [build-system]
18
- requires = ["setuptools>=68"]
19
- build-backend = "setuptools.build_meta"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes