kobo-mcp 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,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .env
10
+ .venv/
11
+ venv/
kobo_mcp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Benjamin Daniels
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,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: kobo-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for KoboToolbox - deploy surveys and fetch submissions
5
+ Project-URL: Homepage, https://github.com/bbdaniels/kobo-mcp
6
+ Project-URL: Documentation, https://bbdaniels.github.io/kobo-mcp/
7
+ Project-URL: Repository, https://github.com/bbdaniels/kobo-mcp
8
+ Project-URL: Issues, https://github.com/bbdaniels/kobo-mcp/issues
9
+ Author-email: Benjamin Daniels <bbdaniels@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: anthropic,claude,kobo,kobotoolbox,mcp,survey,xlsform
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: httpx>=0.27.0
25
+ Requires-Dist: mcp[cli]>=1.0.0
26
+ Description-Content-Type: text/markdown
27
+
28
+ # Kobo MCP
29
+
30
+ [![PyPI version](https://badge.fury.io/py/kobo-mcp.svg)](https://pypi.org/project/kobo-mcp/)
31
+
32
+ An MCP server for KoboToolbox that enables Claude to deploy surveys and fetch submissions.
33
+
34
+ **[Documentation](https://bbdaniels.github.io/kobo-mcp/)**
35
+
36
+ ## Features
37
+
38
+ - **list_forms** - List all KoboToolbox surveys
39
+ - **get_form** - Get detailed form information
40
+ - **get_submissions** - Fetch survey responses
41
+ - **deploy_form** - Upload and deploy XLSForm files
42
+ - **replace_form** - Update existing forms preserving submissions
43
+ - **export_data** - Export data as CSV or Excel
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install kobo-mcp
49
+ ```
50
+
51
+ Or run directly with uvx (no install):
52
+
53
+ ```bash
54
+ uvx kobo-mcp
55
+ ```
56
+
57
+ ## Configuration
58
+
59
+ ### 1. Get your API token
60
+
61
+ Go to [KoboToolbox](https://kf.kobotoolbox.org) → Account Settings → Security → Display
62
+
63
+ ### 2. Add to Claude
64
+
65
+ Add to `~/.claude.json`:
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "kobotoolbox": {
71
+ "type": "stdio",
72
+ "command": "kobo-mcp",
73
+ "env": {
74
+ "KOBO_API_TOKEN": "your-token-here"
75
+ }
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ Or with uvx:
82
+
83
+ ```json
84
+ {
85
+ "mcpServers": {
86
+ "kobotoolbox": {
87
+ "type": "stdio",
88
+ "command": "uvx",
89
+ "args": ["kobo-mcp"],
90
+ "env": {
91
+ "KOBO_API_TOKEN": "your-token-here"
92
+ }
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ For EU server, add `"KOBO_SERVER": "https://eu.kobotoolbox.org"` to env.
99
+
100
+ ## Development
101
+
102
+ ```bash
103
+ git clone https://github.com/bbdaniels/kobo-mcp.git
104
+ cd kobo-mcp
105
+ pip install -e .
106
+ ```
107
+
108
+ ## License
109
+
110
+ MIT
@@ -0,0 +1,83 @@
1
+ # Kobo MCP
2
+
3
+ [![PyPI version](https://badge.fury.io/py/kobo-mcp.svg)](https://pypi.org/project/kobo-mcp/)
4
+
5
+ An MCP server for KoboToolbox that enables Claude to deploy surveys and fetch submissions.
6
+
7
+ **[Documentation](https://bbdaniels.github.io/kobo-mcp/)**
8
+
9
+ ## Features
10
+
11
+ - **list_forms** - List all KoboToolbox surveys
12
+ - **get_form** - Get detailed form information
13
+ - **get_submissions** - Fetch survey responses
14
+ - **deploy_form** - Upload and deploy XLSForm files
15
+ - **replace_form** - Update existing forms preserving submissions
16
+ - **export_data** - Export data as CSV or Excel
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install kobo-mcp
22
+ ```
23
+
24
+ Or run directly with uvx (no install):
25
+
26
+ ```bash
27
+ uvx kobo-mcp
28
+ ```
29
+
30
+ ## Configuration
31
+
32
+ ### 1. Get your API token
33
+
34
+ Go to [KoboToolbox](https://kf.kobotoolbox.org) → Account Settings → Security → Display
35
+
36
+ ### 2. Add to Claude
37
+
38
+ Add to `~/.claude.json`:
39
+
40
+ ```json
41
+ {
42
+ "mcpServers": {
43
+ "kobotoolbox": {
44
+ "type": "stdio",
45
+ "command": "kobo-mcp",
46
+ "env": {
47
+ "KOBO_API_TOKEN": "your-token-here"
48
+ }
49
+ }
50
+ }
51
+ }
52
+ ```
53
+
54
+ Or with uvx:
55
+
56
+ ```json
57
+ {
58
+ "mcpServers": {
59
+ "kobotoolbox": {
60
+ "type": "stdio",
61
+ "command": "uvx",
62
+ "args": ["kobo-mcp"],
63
+ "env": {
64
+ "KOBO_API_TOKEN": "your-token-here"
65
+ }
66
+ }
67
+ }
68
+ }
69
+ ```
70
+
71
+ For EU server, add `"KOBO_SERVER": "https://eu.kobotoolbox.org"` to env.
72
+
73
+ ## Development
74
+
75
+ ```bash
76
+ git clone https://github.com/bbdaniels/kobo-mcp.git
77
+ cd kobo-mcp
78
+ pip install -e .
79
+ ```
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,46 @@
1
+ [project]
2
+ name = "kobo-mcp"
3
+ version = "0.1.0"
4
+ description = "MCP server for KoboToolbox - deploy surveys and fetch submissions"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.10"
8
+ authors = [
9
+ { name = "Benjamin Daniels", email = "bbdaniels@gmail.com" }
10
+ ]
11
+ keywords = ["mcp", "kobotoolbox", "kobo", "survey", "xlsform", "claude", "anthropic"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "Intended Audience :: Science/Research",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Scientific/Engineering",
23
+ ]
24
+ dependencies = [
25
+ "mcp[cli]>=1.0.0",
26
+ "httpx>=0.27.0",
27
+ ]
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/bbdaniels/kobo-mcp"
31
+ Documentation = "https://bbdaniels.github.io/kobo-mcp/"
32
+ Repository = "https://github.com/bbdaniels/kobo-mcp"
33
+ Issues = "https://github.com/bbdaniels/kobo-mcp/issues"
34
+
35
+ [project.scripts]
36
+ kobo-mcp = "kobo_mcp:main"
37
+
38
+ [build-system]
39
+ requires = ["hatchling"]
40
+ build-backend = "hatchling.build"
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/kobo_mcp"]
44
+
45
+ [tool.hatch.build.targets.sdist]
46
+ include = ["src/kobo_mcp"]
@@ -0,0 +1,3 @@
1
+ from .server import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,366 @@
1
+ """KoboToolbox MCP Server - Deploy surveys and fetch submissions."""
2
+
3
+ import json
4
+ import os
5
+ import sys
6
+ from typing import Any
7
+
8
+ import httpx
9
+ from mcp.server.fastmcp import FastMCP
10
+
11
+ # Server instance
12
+ mcp = FastMCP("kobotoolbox")
13
+
14
+ # Configuration from environment
15
+ KOBO_API_TOKEN = os.environ.get("KOBO_API_TOKEN", "")
16
+ KOBO_SERVER = os.environ.get("KOBO_SERVER", "https://kf.kobotoolbox.org")
17
+
18
+
19
+ def get_headers() -> dict[str, str]:
20
+ """Get authorization headers for API requests."""
21
+ if not KOBO_API_TOKEN:
22
+ raise ValueError("KOBO_API_TOKEN environment variable is not set")
23
+ return {"Authorization": f"Token {KOBO_API_TOKEN}"}
24
+
25
+
26
+ def format_asset(asset: dict[str, Any]) -> dict[str, Any]:
27
+ """Format an asset object for display."""
28
+ return {
29
+ "uid": asset.get("uid"),
30
+ "name": asset.get("name"),
31
+ "asset_type": asset.get("asset_type"),
32
+ "deployment_status": asset.get("deployment_status"),
33
+ "submission_count": asset.get("deployment__submission_count", 0),
34
+ "date_created": asset.get("date_created"),
35
+ "date_modified": asset.get("date_modified"),
36
+ "owner": asset.get("owner__username"),
37
+ }
38
+
39
+
40
+ @mcp.tool()
41
+ async def list_forms(search: str | None = None) -> str:
42
+ """List all KoboToolbox forms/surveys.
43
+
44
+ Args:
45
+ search: Optional search term to filter forms by name.
46
+
47
+ Returns:
48
+ JSON list of forms with uid, name, status, and submission count.
49
+ """
50
+ params: dict[str, Any] = {"asset_type": "survey"}
51
+ if search:
52
+ params["q"] = search
53
+
54
+ async with httpx.AsyncClient() as client:
55
+ response = await client.get(
56
+ f"{KOBO_SERVER}/api/v2/assets/",
57
+ headers=get_headers(),
58
+ params=params,
59
+ timeout=30.0,
60
+ )
61
+ response.raise_for_status()
62
+ data = response.json()
63
+
64
+ results = data.get("results", [])
65
+ forms = [format_asset(asset) for asset in results]
66
+ return json.dumps(forms, indent=2)
67
+
68
+
69
+ @mcp.tool()
70
+ async def get_form(form_uid: str) -> str:
71
+ """Get detailed information about a specific form.
72
+
73
+ Args:
74
+ form_uid: The unique identifier (uid) of the form.
75
+
76
+ Returns:
77
+ JSON object with form details including questions/fields.
78
+ """
79
+ async with httpx.AsyncClient() as client:
80
+ response = await client.get(
81
+ f"{KOBO_SERVER}/api/v2/assets/{form_uid}/",
82
+ headers=get_headers(),
83
+ timeout=30.0,
84
+ )
85
+ response.raise_for_status()
86
+ data = response.json()
87
+
88
+ # Extract key information
89
+ result = {
90
+ "uid": data.get("uid"),
91
+ "name": data.get("name"),
92
+ "deployment_status": data.get("deployment_status"),
93
+ "submission_count": data.get("deployment__submission_count", 0),
94
+ "date_created": data.get("date_created"),
95
+ "date_modified": data.get("date_modified"),
96
+ "owner": data.get("owner__username"),
97
+ "content": data.get("content"), # Contains survey structure
98
+ }
99
+ return json.dumps(result, indent=2)
100
+
101
+
102
+ @mcp.tool()
103
+ async def get_submissions(
104
+ form_uid: str,
105
+ limit: int = 100,
106
+ start: int = 0,
107
+ query: str | None = None,
108
+ ) -> str:
109
+ """Get submissions (responses) for a form.
110
+
111
+ Args:
112
+ form_uid: The unique identifier (uid) of the form.
113
+ limit: Maximum number of submissions to return (default 100).
114
+ start: Offset for pagination (default 0).
115
+ query: Optional JSON query string to filter submissions (e.g., '{"field": "value"}').
116
+
117
+ Returns:
118
+ JSON object with count and list of submissions.
119
+ """
120
+ params: dict[str, Any] = {"limit": limit, "start": start}
121
+ if query:
122
+ params["query"] = query
123
+
124
+ async with httpx.AsyncClient() as client:
125
+ response = await client.get(
126
+ f"{KOBO_SERVER}/api/v2/assets/{form_uid}/data/",
127
+ headers=get_headers(),
128
+ params=params,
129
+ timeout=60.0,
130
+ )
131
+ response.raise_for_status()
132
+ data = response.json()
133
+
134
+ return json.dumps(
135
+ {"count": data.get("count", 0), "results": data.get("results", [])},
136
+ indent=2,
137
+ )
138
+
139
+
140
+ @mcp.tool()
141
+ async def deploy_form(file_path: str, form_name: str | None = None) -> str:
142
+ """Upload and deploy an XLSForm to KoboToolbox.
143
+
144
+ Args:
145
+ file_path: Path to the XLSForm (.xlsx) file.
146
+ form_name: Optional name for the form (defaults to filename).
147
+
148
+ Returns:
149
+ JSON object with the created form's uid and deployment status.
150
+ """
151
+ import os.path
152
+
153
+ if not os.path.exists(file_path):
154
+ return json.dumps({"error": f"File not found: {file_path}"})
155
+
156
+ name = form_name or os.path.splitext(os.path.basename(file_path))[0]
157
+
158
+ async with httpx.AsyncClient() as client:
159
+ # Step 1: Upload the XLSForm to create an asset
160
+ with open(file_path, "rb") as f:
161
+ files = {"file": (os.path.basename(file_path), f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
162
+ data = {"name": name}
163
+ response = await client.post(
164
+ f"{KOBO_SERVER}/api/v2/assets/",
165
+ headers=get_headers(),
166
+ files=files,
167
+ data=data,
168
+ timeout=60.0,
169
+ )
170
+ response.raise_for_status()
171
+ asset = response.json()
172
+
173
+ uid = asset.get("uid")
174
+
175
+ # Step 2: Deploy the form
176
+ deploy_response = await client.post(
177
+ f"{KOBO_SERVER}/api/v2/assets/{uid}/deployment/",
178
+ headers=get_headers(),
179
+ json={"active": True},
180
+ timeout=30.0,
181
+ )
182
+ deploy_response.raise_for_status()
183
+
184
+ return json.dumps(
185
+ {
186
+ "uid": uid,
187
+ "name": name,
188
+ "status": "deployed",
189
+ "url": f"{KOBO_SERVER}/#/forms/{uid}",
190
+ },
191
+ indent=2,
192
+ )
193
+
194
+
195
+ @mcp.tool()
196
+ async def replace_form(form_uid: str, file_path: str) -> str:
197
+ """Replace an existing form with a new XLSForm version.
198
+
199
+ This updates the form definition while preserving the form UID and existing submissions.
200
+
201
+ Args:
202
+ form_uid: The unique identifier (uid) of the form to replace.
203
+ file_path: Path to the new XLSForm (.xlsx) file.
204
+
205
+ Returns:
206
+ JSON object with the updated form's uid and deployment status.
207
+ """
208
+ import asyncio
209
+ import os.path
210
+
211
+ if not os.path.exists(file_path):
212
+ return json.dumps({"error": f"File not found: {file_path}"})
213
+
214
+ async with httpx.AsyncClient() as client:
215
+ # Step 1: Create an import task targeting the existing asset
216
+ with open(file_path, "rb") as f:
217
+ files = {"file": (os.path.basename(file_path), f, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
218
+ data = {"destination": f"{KOBO_SERVER}/api/v2/assets/{form_uid}/"}
219
+ response = await client.post(
220
+ f"{KOBO_SERVER}/api/v2/imports/",
221
+ headers=get_headers(),
222
+ files=files,
223
+ data=data,
224
+ timeout=60.0,
225
+ )
226
+ response.raise_for_status()
227
+ import_task = response.json()
228
+
229
+ import_uid = import_task.get("uid")
230
+
231
+ # Step 2: Poll for import completion (max 60 seconds)
232
+ for _ in range(60):
233
+ status_response = await client.get(
234
+ f"{KOBO_SERVER}/api/v2/imports/{import_uid}/",
235
+ headers=get_headers(),
236
+ timeout=30.0,
237
+ )
238
+ status_response.raise_for_status()
239
+ status_data = status_response.json()
240
+
241
+ if status_data.get("status") == "complete":
242
+ break
243
+ elif status_data.get("status") == "error":
244
+ return json.dumps(
245
+ {"status": "error", "message": status_data.get("messages", {})},
246
+ indent=2,
247
+ )
248
+
249
+ await asyncio.sleep(1)
250
+ else:
251
+ return json.dumps(
252
+ {"status": "timeout", "message": "Import is still processing."},
253
+ indent=2,
254
+ )
255
+
256
+ # Step 3: Redeploy the form to make changes live
257
+ deploy_response = await client.patch(
258
+ f"{KOBO_SERVER}/api/v2/assets/{form_uid}/deployment/",
259
+ headers=get_headers(),
260
+ json={"active": True},
261
+ timeout=30.0,
262
+ )
263
+ deploy_response.raise_for_status()
264
+
265
+ # Step 4: Get updated asset info
266
+ asset_response = await client.get(
267
+ f"{KOBO_SERVER}/api/v2/assets/{form_uid}/",
268
+ headers=get_headers(),
269
+ timeout=30.0,
270
+ )
271
+ asset_response.raise_for_status()
272
+ asset = asset_response.json()
273
+
274
+ return json.dumps(
275
+ {
276
+ "uid": form_uid,
277
+ "name": asset.get("name"),
278
+ "status": "redeployed",
279
+ "submission_count": asset.get("deployment__submission_count", 0),
280
+ "url": f"{KOBO_SERVER}/#/forms/{form_uid}",
281
+ },
282
+ indent=2,
283
+ )
284
+
285
+
286
+ @mcp.tool()
287
+ async def export_data(
288
+ form_uid: str,
289
+ export_type: str = "csv",
290
+ include_labels: bool = True,
291
+ ) -> str:
292
+ """Create and download a data export for a form.
293
+
294
+ Args:
295
+ form_uid: The unique identifier (uid) of the form.
296
+ export_type: Export format - 'csv' or 'xls' (default 'csv').
297
+ include_labels: Include question labels in headers (default True).
298
+
299
+ Returns:
300
+ JSON object with export download URL or the data itself for small exports.
301
+ """
302
+ export_settings = {
303
+ "fields_from_all_versions": True,
304
+ "group_sep": "/",
305
+ "hierarchy_in_labels": include_labels,
306
+ "multiple_select": "both",
307
+ "type": export_type,
308
+ }
309
+
310
+ async with httpx.AsyncClient() as client:
311
+ # Create export
312
+ response = await client.post(
313
+ f"{KOBO_SERVER}/api/v2/assets/{form_uid}/exports/",
314
+ headers=get_headers(),
315
+ json=export_settings,
316
+ timeout=30.0,
317
+ )
318
+ response.raise_for_status()
319
+ export_data = response.json()
320
+
321
+ export_uid = export_data.get("uid")
322
+
323
+ # Poll for completion (max 30 seconds)
324
+ import asyncio
325
+
326
+ for _ in range(30):
327
+ status_response = await client.get(
328
+ f"{KOBO_SERVER}/api/v2/assets/{form_uid}/exports/{export_uid}/",
329
+ headers=get_headers(),
330
+ timeout=30.0,
331
+ )
332
+ status_response.raise_for_status()
333
+ status_data = status_response.json()
334
+
335
+ if status_data.get("status") == "complete":
336
+ return json.dumps(
337
+ {
338
+ "status": "complete",
339
+ "download_url": status_data.get("result"),
340
+ "type": export_type,
341
+ },
342
+ indent=2,
343
+ )
344
+ elif status_data.get("status") == "error":
345
+ return json.dumps(
346
+ {"status": "error", "message": status_data.get("messages", {})},
347
+ indent=2,
348
+ )
349
+
350
+ await asyncio.sleep(1)
351
+
352
+ return json.dumps(
353
+ {"status": "pending", "message": "Export is still processing. Try again later."},
354
+ indent=2,
355
+ )
356
+
357
+
358
+ def main():
359
+ """Run the MCP server."""
360
+ # Log to stderr (stdout is reserved for MCP protocol)
361
+ print("Starting KoboToolbox MCP server...", file=sys.stderr)
362
+ mcp.run(transport="stdio")
363
+
364
+
365
+ if __name__ == "__main__":
366
+ main()