kobo-mcp 0.1.0__py3-none-any.whl
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.
kobo_mcp/__init__.py
ADDED
kobo_mcp/server.py
ADDED
|
@@ -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()
|
|
@@ -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
|
+
[](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,7 @@
|
|
|
1
|
+
kobo_mcp/__init__.py,sha256=kd5Da6RpqW39wTlqulTHiEdOPNmPaxxs4Ke14XeuyIg,45
|
|
2
|
+
kobo_mcp/server.py,sha256=XfsAt83ZBT7Ts3g1Mzf2vAZosOA7LZVRVA3FXwWUQV4,11464
|
|
3
|
+
kobo_mcp-0.1.0.dist-info/METADATA,sha256=kGVWbEW9lfMCLJ1kt41SSSO_wAncDlNZk8IVewFU-WU,2646
|
|
4
|
+
kobo_mcp-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
5
|
+
kobo_mcp-0.1.0.dist-info/entry_points.txt,sha256=rTE1u4k2Y3_uPFS-posQICKWs2J5hIuhyes4rnxMlMg,43
|
|
6
|
+
kobo_mcp-0.1.0.dist-info/licenses/LICENSE,sha256=rAOeMCaTN7BZ21NpEoTbIRI134qQbKjwo580nvv3c7k,1073
|
|
7
|
+
kobo_mcp-0.1.0.dist-info/RECORD,,
|
|
@@ -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.
|