@kubbi.ai/mcp 0.1.0
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.
- package/README.md +172 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.js +35 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/tools/claim.d.ts +2 -0
- package/dist/tools/claim.js +59 -0
- package/dist/tools/delete.d.ts +3 -0
- package/dist/tools/delete.js +32 -0
- package/dist/tools/get.d.ts +3 -0
- package/dist/tools/get.js +29 -0
- package/dist/tools/inspect.d.ts +2 -0
- package/dist/tools/inspect.js +62 -0
- package/dist/tools/send.d.ts +3 -0
- package/dist/tools/send.js +142 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# @kubbi.ai/mcp
|
|
2
|
+
|
|
3
|
+
MCP server for [Kubbi](https://kubbi.ai) — send, claim, and inspect ephemeral encrypted payloads from any AI agent. Built on the [@kubbi.ai/sdk](https://www.npmjs.com/package/@kubbi.ai/sdk) TypeScript SDK.
|
|
4
|
+
|
|
5
|
+
## What This Does
|
|
6
|
+
|
|
7
|
+
This MCP server gives AI agents (Claude, Cursor, VS Code Copilot, etc.) the ability to:
|
|
8
|
+
|
|
9
|
+
- **Claim** a kubbi — retrieve encrypted content using just a claim URL (zero configuration)
|
|
10
|
+
- **Inspect** a kubbi — check metadata without consuming it
|
|
11
|
+
- **Send** a kubbi — create an ephemeral payload and get a shareable claim URL
|
|
12
|
+
- **Get/Delete** kubbis you've created
|
|
13
|
+
|
|
14
|
+
The consumer tools (`kubbi_inspect`, `kubbi_claim`) need **zero auth** — any agent can receive and consume kubbis with no API key, no account, no setup. This is the killer feature for agent-to-agent handoffs.
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
### Claude Desktop
|
|
19
|
+
|
|
20
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"mcpServers": {
|
|
25
|
+
"kubbi": {
|
|
26
|
+
"command": "npx",
|
|
27
|
+
"args": ["-y", "@kubbi.ai/mcp"],
|
|
28
|
+
"env": {
|
|
29
|
+
"KUBBI_API_KEY": "kb_your_key_here"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Cursor
|
|
37
|
+
|
|
38
|
+
Add to `.cursor/mcp.json` in your project or global settings:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"kubbi": {
|
|
44
|
+
"command": "npx",
|
|
45
|
+
"args": ["-y", "@kubbi.ai/mcp"],
|
|
46
|
+
"env": {
|
|
47
|
+
"KUBBI_API_KEY": "kb_your_key_here"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### VS Code
|
|
55
|
+
|
|
56
|
+
Add to `.vscode/mcp.json`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"servers": {
|
|
61
|
+
"kubbi": {
|
|
62
|
+
"type": "stdio",
|
|
63
|
+
"command": "npx",
|
|
64
|
+
"args": ["-y", "@kubbi.ai/mcp"],
|
|
65
|
+
"env": {
|
|
66
|
+
"KUBBI_API_KEY": "kb_your_key_here"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> **Consumers only?** Omit `KUBBI_API_KEY` entirely. The `kubbi_inspect` and `kubbi_claim` tools will work with no configuration at all.
|
|
74
|
+
|
|
75
|
+
## Environment Variables
|
|
76
|
+
|
|
77
|
+
| Variable | Required | Description |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `KUBBI_API_KEY` | No* | API key for creating kubbis. Create one at [dashboard.kubbi.ai](https://dashboard.kubbi.ai). Only needed for producer tools. |
|
|
80
|
+
| `KUBBI_BASE_URL` | No | API base URL (default: `https://api.kubbi.ai`) |
|
|
81
|
+
|
|
82
|
+
*Producer tools (`kubbi_send`, `kubbi_send_files`, `kubbi_get`, `kubbi_delete`) require an API key. Consumer tools work without one.
|
|
83
|
+
|
|
84
|
+
## Tools
|
|
85
|
+
|
|
86
|
+
### kubbi_inspect (no auth)
|
|
87
|
+
|
|
88
|
+
Inspect a kubbi without consuming it. Returns metadata: status, content type, retrieval count, expiry. Does not count as a retrieval.
|
|
89
|
+
|
|
90
|
+
| Parameter | Type | Description |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `claimUrl` | string | Full claim URL or bare claim token |
|
|
93
|
+
|
|
94
|
+
### kubbi_claim (no auth)
|
|
95
|
+
|
|
96
|
+
Claim a kubbi and retrieve its decrypted content. Counts as a retrieval — if `max_retrievals` is reached, the content is permanently destroyed.
|
|
97
|
+
|
|
98
|
+
| Parameter | Type | Description |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `claimUrl` | string | Full claim URL or bare claim token |
|
|
101
|
+
|
|
102
|
+
### kubbi_send (requires API key)
|
|
103
|
+
|
|
104
|
+
Create an ephemeral encrypted kubbi. Returns a claim URL that any consumer can use.
|
|
105
|
+
|
|
106
|
+
| Parameter | Type | Default | Description |
|
|
107
|
+
|---|---|---|---|
|
|
108
|
+
| `content` | string or object | — | Payload to send (max 16KB) |
|
|
109
|
+
| `contentType` | string | `application/json` | `text/plain` or `application/json` |
|
|
110
|
+
| `ttlSeconds` | number | `3600` | Time-to-live in seconds (60–86400) |
|
|
111
|
+
| `maxRetrievals` | number | unlimited | Max claims before auto-burn. `1` = burn-after-read |
|
|
112
|
+
| `metadata` | object | — | Arbitrary metadata (max 1KB) |
|
|
113
|
+
|
|
114
|
+
### kubbi_send_files (requires API key)
|
|
115
|
+
|
|
116
|
+
Create a multi-file kubbi package — up to 5 files (5 MB total) encrypted together with a single claim URL. For binary files, base64-encode the content and set `encoding` to `"base64"`.
|
|
117
|
+
|
|
118
|
+
| Parameter | Type | Default | Description |
|
|
119
|
+
|---|---|---|---|
|
|
120
|
+
| `files` | array | — | Array of file objects (1–5, max 5 MB total) |
|
|
121
|
+
| `files[].name` | string | — | Filename (e.g. `"config.json"`) |
|
|
122
|
+
| `files[].content` | string | — | File content (text or base64-encoded binary) |
|
|
123
|
+
| `files[].contentType` | string | `text/plain` | MIME type (e.g. `application/json`, `image/png`) |
|
|
124
|
+
| `files[].role` | string | — | Semantic role: `instructions`, `data`, `context`, `config`, or `attachment` |
|
|
125
|
+
| `files[].encoding` | string | — | Set to `base64` if content is base64-encoded |
|
|
126
|
+
| `ttlSeconds` | number | `3600` | Time-to-live in seconds (60–86400) |
|
|
127
|
+
| `maxRetrievals` | number | unlimited | Max claims before auto-burn |
|
|
128
|
+
| `metadata` | object | — | Arbitrary metadata (max 1KB) |
|
|
129
|
+
|
|
130
|
+
### kubbi_get (requires API key)
|
|
131
|
+
|
|
132
|
+
Get detailed metadata for a kubbi you created.
|
|
133
|
+
|
|
134
|
+
| Parameter | Type | Description |
|
|
135
|
+
|---|---|---|
|
|
136
|
+
| `id` | string | Kubbi ID (UUID) |
|
|
137
|
+
|
|
138
|
+
### kubbi_delete (requires API key)
|
|
139
|
+
|
|
140
|
+
Delete a kubbi, immediately wiping the encrypted payload.
|
|
141
|
+
|
|
142
|
+
| Parameter | Type | Description |
|
|
143
|
+
|---|---|---|
|
|
144
|
+
| `id` | string | Kubbi ID (UUID) |
|
|
145
|
+
|
|
146
|
+
## Example: Agent-to-Agent Handoff
|
|
147
|
+
|
|
148
|
+
**Agent A** (has API key) creates a kubbi and passes the claim URL in its response:
|
|
149
|
+
|
|
150
|
+
> "I've stored the analysis results in a secure kubbi. Here's the claim URL: `https://api.kubbi.ai/r/x7k9m2...`"
|
|
151
|
+
|
|
152
|
+
**Agent B** (no API key, no setup) claims it:
|
|
153
|
+
|
|
154
|
+
> Tool call: `kubbi_claim({ claimUrl: "https://api.kubbi.ai/r/x7k9m2..." })`
|
|
155
|
+
|
|
156
|
+
The content is retrieved and automatically destroyed (if `maxRetrievals: 1`).
|
|
157
|
+
|
|
158
|
+
## Development
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
git clone https://github.com/kubbi/kubbi-mcp.git
|
|
162
|
+
cd kubbi-mcp
|
|
163
|
+
npm install
|
|
164
|
+
npm run build
|
|
165
|
+
|
|
166
|
+
# Test locally with MCP Inspector
|
|
167
|
+
npx @modelcontextprotocol/inspector node dist/index.js
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { KubbiApiError, KubbiValidationError, KubbiQuotaExceededError, KubbiRateLimitError, KubbiAuthenticationError, KubbiNotFoundError, KubbiGoneError, KubbiConflictError, KubbiNetworkError, } from "@kubbi.ai/sdk";
|
|
2
|
+
export function formatError(err) {
|
|
3
|
+
let message;
|
|
4
|
+
if (err instanceof KubbiValidationError) {
|
|
5
|
+
message = `Validation error: ${err.messages?.join(", ") ?? err.message}`;
|
|
6
|
+
}
|
|
7
|
+
else if (err instanceof KubbiQuotaExceededError) {
|
|
8
|
+
message = `Quota exceeded: ${err.message}`;
|
|
9
|
+
}
|
|
10
|
+
else if (err instanceof KubbiRateLimitError) {
|
|
11
|
+
message = `Rate limited: ${err.message}. Try again shortly.`;
|
|
12
|
+
}
|
|
13
|
+
else if (err instanceof KubbiAuthenticationError) {
|
|
14
|
+
message = `Authentication failed: ${err.message}. Check your KUBBI_API_KEY.`;
|
|
15
|
+
}
|
|
16
|
+
else if (err instanceof KubbiConflictError) {
|
|
17
|
+
message = `Conflict: ${err.message}`;
|
|
18
|
+
}
|
|
19
|
+
else if (err instanceof KubbiGoneError) {
|
|
20
|
+
message = `This kubbi has been consumed or expired (410 Gone). ${err.message}`;
|
|
21
|
+
}
|
|
22
|
+
else if (err instanceof KubbiNotFoundError) {
|
|
23
|
+
message = `Not found (404). ${err.message}`;
|
|
24
|
+
}
|
|
25
|
+
else if (err instanceof KubbiNetworkError) {
|
|
26
|
+
message = `Network error contacting Kubbi API: ${err.message}`;
|
|
27
|
+
}
|
|
28
|
+
else if (err instanceof KubbiApiError) {
|
|
29
|
+
message = `Kubbi API error (${err.status}): ${err.message}`;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
message = `Unexpected error: ${err instanceof Error ? err.message : String(err)}`;
|
|
33
|
+
}
|
|
34
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
35
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { KubbiClient } from "@kubbi.ai/sdk";
|
|
5
|
+
import { registerInspect } from "./tools/inspect.js";
|
|
6
|
+
import { registerClaim } from "./tools/claim.js";
|
|
7
|
+
import { registerSend } from "./tools/send.js";
|
|
8
|
+
import { registerGet } from "./tools/get.js";
|
|
9
|
+
import { registerDelete } from "./tools/delete.js";
|
|
10
|
+
const apiKey = process.env.KUBBI_API_KEY;
|
|
11
|
+
const baseUrl = process.env.KUBBI_BASE_URL ?? "https://api.kubbi.ai";
|
|
12
|
+
const client = apiKey
|
|
13
|
+
? new KubbiClient({ apiKey, baseUrl })
|
|
14
|
+
: null;
|
|
15
|
+
const server = new McpServer({
|
|
16
|
+
name: "kubbi",
|
|
17
|
+
version: "0.1.0",
|
|
18
|
+
});
|
|
19
|
+
registerInspect(server, baseUrl);
|
|
20
|
+
registerClaim(server, baseUrl);
|
|
21
|
+
registerSend(server, client);
|
|
22
|
+
registerGet(server, client);
|
|
23
|
+
registerDelete(server, client);
|
|
24
|
+
const transport = new StdioServerTransport();
|
|
25
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { KubbiClient, isPackageClaim } from "@kubbi.ai/sdk";
|
|
3
|
+
import { formatError } from "../errors.js";
|
|
4
|
+
export function registerClaim(server, baseUrl) {
|
|
5
|
+
server.registerTool("kubbi_claim", {
|
|
6
|
+
description: "Claim a kubbi and retrieve its decrypted content. This COUNTS as a retrieval — if max_retrievals is reached, the content is permanently destroyed afterward. Works for both single-content and multi-file kubbis. Accepts a full claim URL or a bare claim token. No API key required.",
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
claimUrl: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe("Full claim URL (e.g. https://api.kubbi.ai/r/abc123) or bare claim token"),
|
|
11
|
+
}),
|
|
12
|
+
}, async ({ claimUrl }) => {
|
|
13
|
+
try {
|
|
14
|
+
const result = await KubbiClient.claim(claimUrl, { baseUrl });
|
|
15
|
+
if (isPackageClaim(result)) {
|
|
16
|
+
const lines = [
|
|
17
|
+
`Kubbi package claimed (${result.fileCount} file${result.fileCount === 1 ? "" : "s"}).`,
|
|
18
|
+
`Created: ${result.createdAt}`,
|
|
19
|
+
`Expires: ${result.expiresAt}`,
|
|
20
|
+
];
|
|
21
|
+
if (result.metadata && Object.keys(result.metadata).length > 0) {
|
|
22
|
+
lines.push(`Metadata: ${JSON.stringify(result.metadata)}`);
|
|
23
|
+
}
|
|
24
|
+
for (const file of result.files) {
|
|
25
|
+
lines.push("");
|
|
26
|
+
lines.push(`--- ${file.name} (${file.contentType}, ${file.sizeBytes} bytes${file.role ? `, role: ${file.role}` : ""}) ---`);
|
|
27
|
+
if (file.encoding === "base64") {
|
|
28
|
+
lines.push(`[base64-encoded binary, ${file.sizeBytes} bytes]`);
|
|
29
|
+
lines.push(file.content);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
lines.push(file.content);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const contentStr = typeof result.content === "string"
|
|
40
|
+
? result.content
|
|
41
|
+
: JSON.stringify(result.content, null, 2);
|
|
42
|
+
const parts = [
|
|
43
|
+
`Content-Type: ${result.contentType}`,
|
|
44
|
+
`Created: ${result.createdAt}`,
|
|
45
|
+
`Expires: ${result.expiresAt}`,
|
|
46
|
+
];
|
|
47
|
+
if (result.metadata && Object.keys(result.metadata).length > 0) {
|
|
48
|
+
parts.push(`Metadata: ${JSON.stringify(result.metadata)}`);
|
|
49
|
+
}
|
|
50
|
+
parts.push("", "--- Content ---", contentStr);
|
|
51
|
+
return {
|
|
52
|
+
content: [{ type: "text", text: parts.join("\n") }],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
return formatError(err);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatError } from "../errors.js";
|
|
3
|
+
const NO_KEY_MSG = "KUBBI_API_KEY is not configured. Set it in your MCP server environment to delete kubbis.";
|
|
4
|
+
export function registerDelete(server, client) {
|
|
5
|
+
server.registerTool("kubbi_delete", {
|
|
6
|
+
description: "Delete (burn) a kubbi you created. Immediately wipes the encrypted payload. Any subsequent claim attempts will receive 410 Gone. Requires KUBBI_API_KEY.",
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
id: z.string().describe("The kubbi ID (UUID) to delete"),
|
|
9
|
+
}),
|
|
10
|
+
}, async ({ id }) => {
|
|
11
|
+
if (!client) {
|
|
12
|
+
return {
|
|
13
|
+
content: [{ type: "text", text: NO_KEY_MSG }],
|
|
14
|
+
isError: true,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const result = await client.delete(id);
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: "text",
|
|
23
|
+
text: `Kubbi ${id} deleted. Status: ${result.status}`,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
return formatError(err);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatError } from "../errors.js";
|
|
3
|
+
const NO_KEY_MSG = "KUBBI_API_KEY is not configured. Set it in your MCP server environment to view kubbi details.";
|
|
4
|
+
export function registerGet(server, client) {
|
|
5
|
+
server.registerTool("kubbi_get", {
|
|
6
|
+
description: "Get detailed metadata for a kubbi you created (by ID). Shows status, retrieval count, timestamps, and whether it has been claimed. Requires KUBBI_API_KEY.",
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
id: z.string().describe("The kubbi ID (UUID) returned when it was created"),
|
|
9
|
+
}),
|
|
10
|
+
}, async ({ id }) => {
|
|
11
|
+
if (!client) {
|
|
12
|
+
return {
|
|
13
|
+
content: [{ type: "text", text: NO_KEY_MSG }],
|
|
14
|
+
isError: true,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
const result = await client.get(id);
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return formatError(err);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { KubbiClient, isPackageInspect } from "@kubbi.ai/sdk";
|
|
3
|
+
import { formatError } from "../errors.js";
|
|
4
|
+
export function registerInspect(server, baseUrl) {
|
|
5
|
+
server.registerTool("kubbi_inspect", {
|
|
6
|
+
description: "Inspect a kubbi without consuming it. Returns metadata (status, content type, retrieval count, expiry). For multi-file kubbis, also shows the file manifest. Does NOT count as a retrieval — safe to call repeatedly. Accepts a full claim URL or a bare claim token. No API key required.",
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
claimUrl: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe("Full claim URL (e.g. https://api.kubbi.ai/r/abc123) or bare claim token"),
|
|
11
|
+
}),
|
|
12
|
+
}, async ({ claimUrl }) => {
|
|
13
|
+
try {
|
|
14
|
+
const result = await KubbiClient.inspect(claimUrl, { baseUrl });
|
|
15
|
+
if (isPackageInspect(result)) {
|
|
16
|
+
const lines = [
|
|
17
|
+
`Kubbi Package (${result.fileCount} file${result.fileCount === 1 ? "" : "s"}, ${result.totalSizeBytes} bytes total)`,
|
|
18
|
+
`Status: ${result.status}`,
|
|
19
|
+
`Max Retrievals: ${result.maxRetrievals ?? "unlimited"}`,
|
|
20
|
+
`Retrieval Count: ${result.retrievalCount}`,
|
|
21
|
+
`Remaining Reads: ${result.remainingReads ?? "unlimited"}`,
|
|
22
|
+
`Created: ${result.createdAt}`,
|
|
23
|
+
`Expires: ${result.expiresAt}`,
|
|
24
|
+
];
|
|
25
|
+
if (result.metadata && Object.keys(result.metadata).length > 0) {
|
|
26
|
+
lines.push(`Metadata: ${JSON.stringify(result.metadata)}`);
|
|
27
|
+
}
|
|
28
|
+
lines.push("", "Files:");
|
|
29
|
+
for (const file of result.files) {
|
|
30
|
+
const rolePart = file.role ? ` (${file.role})` : "";
|
|
31
|
+
lines.push(` - ${file.name} [${file.contentType}, ${file.sizeBytes} bytes]${rolePart}`);
|
|
32
|
+
}
|
|
33
|
+
if (result.claim) {
|
|
34
|
+
lines.push("", `Claim: POST ${result.claim.url}`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const lines = [
|
|
41
|
+
`Status: ${result.status}`,
|
|
42
|
+
`Content-Type: ${result.contentType}`,
|
|
43
|
+
`Max Retrievals: ${result.maxRetrievals ?? "unlimited"}`,
|
|
44
|
+
`Retrieval Count: ${result.retrievalCount}`,
|
|
45
|
+
`Created: ${result.createdAt}`,
|
|
46
|
+
`Expires: ${result.expiresAt}`,
|
|
47
|
+
];
|
|
48
|
+
if (result.metadata && Object.keys(result.metadata).length > 0) {
|
|
49
|
+
lines.push(`Metadata: ${JSON.stringify(result.metadata)}`);
|
|
50
|
+
}
|
|
51
|
+
if (result.claim) {
|
|
52
|
+
lines.push("", `Claim: POST ${result.claim.url}`);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return formatError(err);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { formatError } from "../errors.js";
|
|
3
|
+
const NO_KEY_MSG = "KUBBI_API_KEY is not configured. Set it in your MCP server environment to create kubbis. Consumer tools (kubbi_inspect, kubbi_claim) work without a key.";
|
|
4
|
+
export function registerSend(server, client) {
|
|
5
|
+
server.registerTool("kubbi_send", {
|
|
6
|
+
description: "Create a kubbi — an ephemeral, encrypted payload with a unique claim URL. Share the claim URL with any consumer (agent, service, human). The consumer can retrieve the content with no API key. Requires KUBBI_API_KEY.",
|
|
7
|
+
inputSchema: z.object({
|
|
8
|
+
content: z
|
|
9
|
+
.union([z.string(), z.record(z.unknown())])
|
|
10
|
+
.describe("Payload to send — a string or JSON object (max 16KB)"),
|
|
11
|
+
contentType: z
|
|
12
|
+
.enum(["text/plain", "application/json"])
|
|
13
|
+
.default("application/json")
|
|
14
|
+
.describe("MIME type of the content"),
|
|
15
|
+
ttlSeconds: z
|
|
16
|
+
.number()
|
|
17
|
+
.min(60)
|
|
18
|
+
.max(86400)
|
|
19
|
+
.default(3600)
|
|
20
|
+
.describe("Time-to-live in seconds (60–86400). Default: 3600 (1 hour)"),
|
|
21
|
+
maxRetrievals: z
|
|
22
|
+
.number()
|
|
23
|
+
.min(1)
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Max number of claims before auto-burn. Set to 1 for burn-after-read. Omit for unlimited."),
|
|
26
|
+
metadata: z
|
|
27
|
+
.record(z.unknown())
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("Arbitrary metadata attached to the kubbi (max 1KB)"),
|
|
30
|
+
}),
|
|
31
|
+
}, async ({ content, contentType, ttlSeconds, maxRetrievals, metadata }) => {
|
|
32
|
+
if (!client) {
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text", text: NO_KEY_MSG }],
|
|
35
|
+
isError: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const result = await client.send({
|
|
40
|
+
content,
|
|
41
|
+
contentType,
|
|
42
|
+
ttlSeconds,
|
|
43
|
+
maxRetrievals,
|
|
44
|
+
metadata,
|
|
45
|
+
});
|
|
46
|
+
const lines = [
|
|
47
|
+
"Kubbi created successfully.",
|
|
48
|
+
"",
|
|
49
|
+
`Claim URL: ${result.claimUrl}`,
|
|
50
|
+
`ID: ${result.id}`,
|
|
51
|
+
`Status: ${result.status}`,
|
|
52
|
+
`Content-Type: ${result.contentType}`,
|
|
53
|
+
`Max Retrievals: ${result.maxRetrievals ?? "unlimited"}`,
|
|
54
|
+
`Expires: ${result.expiresAt}`,
|
|
55
|
+
];
|
|
56
|
+
if (result.metadata && Object.keys(result.metadata).length > 0) {
|
|
57
|
+
lines.push(`Metadata: ${JSON.stringify(result.metadata)}`);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return formatError(err);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
server.registerTool("kubbi_send_files", {
|
|
68
|
+
description: "Create a multi-file kubbi package — up to 5 files (5 MB total) encrypted together with a single claim URL. For text files, pass content as a string. For binary files, base64-encode the content and set encoding to 'base64'. Requires KUBBI_API_KEY.",
|
|
69
|
+
inputSchema: z.object({
|
|
70
|
+
files: z
|
|
71
|
+
.array(z.object({
|
|
72
|
+
name: z.string().describe("Filename (e.g. 'config.json', 'readme.md')"),
|
|
73
|
+
content: z.string().describe("File content as a string (text) or base64-encoded string (binary)"),
|
|
74
|
+
contentType: z
|
|
75
|
+
.string()
|
|
76
|
+
.default("text/plain")
|
|
77
|
+
.describe("MIME type (e.g. 'text/plain', 'application/json', 'image/png')"),
|
|
78
|
+
role: z
|
|
79
|
+
.enum(["instructions", "data", "context", "config", "attachment"])
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("Semantic role of the file"),
|
|
82
|
+
encoding: z
|
|
83
|
+
.enum(["base64"])
|
|
84
|
+
.optional()
|
|
85
|
+
.describe("Set to 'base64' if content is base64-encoded binary"),
|
|
86
|
+
}))
|
|
87
|
+
.min(1)
|
|
88
|
+
.max(5)
|
|
89
|
+
.describe("Array of files (1–5, max 5 MB total)"),
|
|
90
|
+
ttlSeconds: z
|
|
91
|
+
.number()
|
|
92
|
+
.min(60)
|
|
93
|
+
.max(86400)
|
|
94
|
+
.default(3600)
|
|
95
|
+
.describe("Time-to-live in seconds (60–86400). Default: 3600 (1 hour)"),
|
|
96
|
+
maxRetrievals: z
|
|
97
|
+
.number()
|
|
98
|
+
.min(1)
|
|
99
|
+
.optional()
|
|
100
|
+
.describe("Max number of claims before auto-burn. Omit for unlimited."),
|
|
101
|
+
metadata: z
|
|
102
|
+
.record(z.unknown())
|
|
103
|
+
.optional()
|
|
104
|
+
.describe("Arbitrary metadata attached to the kubbi (max 1KB)"),
|
|
105
|
+
}),
|
|
106
|
+
}, async ({ files, ttlSeconds, maxRetrievals, metadata }) => {
|
|
107
|
+
if (!client) {
|
|
108
|
+
return {
|
|
109
|
+
content: [{ type: "text", text: NO_KEY_MSG }],
|
|
110
|
+
isError: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const result = await client.sendFiles({
|
|
115
|
+
files,
|
|
116
|
+
ttlSeconds,
|
|
117
|
+
maxRetrievals,
|
|
118
|
+
metadata,
|
|
119
|
+
});
|
|
120
|
+
const lines = [
|
|
121
|
+
"Multi-file kubbi created successfully.",
|
|
122
|
+
"",
|
|
123
|
+
`Claim URL: ${result.claimUrl}`,
|
|
124
|
+
`ID: ${result.id}`,
|
|
125
|
+
`Status: ${result.status}`,
|
|
126
|
+
`Files: ${result.fileCount}`,
|
|
127
|
+
`Total Size: ${result.totalSizeBytes} bytes`,
|
|
128
|
+
`Max Retrievals: ${result.maxRetrievals ?? "unlimited"}`,
|
|
129
|
+
`Expires: ${result.expiresAt}`,
|
|
130
|
+
];
|
|
131
|
+
if (result.metadata && Object.keys(result.metadata).length > 0) {
|
|
132
|
+
lines.push(`Metadata: ${JSON.stringify(result.metadata)}`);
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
return formatError(err);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kubbi.ai/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Kubbi — send, claim, and inspect ephemeral encrypted payloads from any AI agent",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kubbi-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"dev": "tsx src/index.ts",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"kubbi",
|
|
19
|
+
"mcp",
|
|
20
|
+
"model-context-protocol",
|
|
21
|
+
"ephemeral",
|
|
22
|
+
"encrypted",
|
|
23
|
+
"payload",
|
|
24
|
+
"handoff",
|
|
25
|
+
"agent",
|
|
26
|
+
"ai",
|
|
27
|
+
"tool"
|
|
28
|
+
],
|
|
29
|
+
"author": "Kubbi",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=20.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@kubbi.ai/sdk": "^0.1.0",
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
37
|
+
"zod": "^3.25.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.0.0",
|
|
41
|
+
"tsx": "^4.0.0",
|
|
42
|
+
"typescript": "^5.6.0"
|
|
43
|
+
}
|
|
44
|
+
}
|