tunc-mcp 0.0.0-stage → 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 CHANGED
@@ -1,3 +1,170 @@
1
- # Temporary Holding Version
1
+ # tunc-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Agent memory, artifact storage, and document editing for AI agents — over MCP.**
4
+
5
+ A small, dependency-free MCP (Model Context Protocol) client for three hosted
6
+ services that AI agents and multi-agent systems actually need:
7
+
8
+ | Service | What it is | MCP endpoint |
9
+ |---|---|---|
10
+ | **Tunc Orb** | **Agent memory database** — durable records, logs and structured state shared by every agent you run | `https://mcp.tuncorb.com/mcp` |
11
+ | **Tunc Art** | **Artifact storage and private file sharing** — agent-generated files behind short private links | `https://mcp.tuncart.com/mcp` |
12
+ | **Tunc Doc** | **Documents, spreadsheets and slide decks** — import/export DOCX, XLSX, PPTX, PDF | `https://mcp.tuncdoc.com/mcp` |
13
+
14
+ All three are also reachable over **A2A** (agent-to-agent) and plain **REST**, so
15
+ the same account works whether your agent speaks MCP, A2A, or HTTP.
16
+
17
+ ## Why this exists
18
+
19
+ Most agent stacks lose state the moment a context window rolls over. Writes from
20
+ two agents at once silently clobber each other. Artifacts end up as files on
21
+ someone's laptop. This gives an agent three boring, reliable primitives instead:
22
+
23
+ - **Memory that survives the conversation.** Records persist across sessions,
24
+ across agents, across models. `data_read` / `data_write` / `data_update` /
25
+ `data_delete`, with revision-guarded writes.
26
+ - **Artifacts with links you can send.** Upload a file, get a short private URL
27
+ back. No bucket configuration.
28
+ - **Documents an agent can actually produce.** Real DOCX, XLSX and PPTX, not
29
+ markdown pretending to be a report.
30
+
31
+ ### Concurrency is the interesting part
32
+
33
+ `data_update` accepts `if_revision`. Two agents writing the same record do not
34
+ race: the second gets a `revision_conflict`, re-reads, and retries. That turns a
35
+ lost-update bug — the classic multi-agent failure where a plausible but wrong
36
+ number survives — into a loud retry. If you run agent swarms that share state,
37
+ use it.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ npm install tunc-mcp
43
+ ```
44
+
45
+ Node 18 or newer. No dependencies. Get an API key at
46
+ <https://tunchub.com/account/keys/new> — you can scope it read-only.
47
+
48
+ ## Use
49
+
50
+ ```js
51
+ import { orb, art, doc } from 'tunc-mcp';
52
+
53
+ const memory = orb(); // reads TUNC_API_KEY
54
+
55
+ // Agent memory: durable records, revision-guarded
56
+ await memory.callTool('data_write', {
57
+ name: 'contacts/ada',
58
+ about: 'Primary contact, Example Ltd',
59
+ value: { email: 'ada@example.com', timezone: 'Europe/London' },
60
+ });
61
+
62
+ // Artifacts: store a file, get a private link
63
+ const files = art();
64
+ await files.callTool('art_write', {
65
+ title: 'quarterly-report.pdf',
66
+ mime_type: 'application/pdf',
67
+ content_base64: base64Bytes,
68
+ });
69
+
70
+ // Documents: build something a human can open and edit
71
+ const docs = doc();
72
+ await docs.callTool('doc_create', {
73
+ title: 'Q4 summary',
74
+ markdown: '# Q4\n\nRevenue up 12%.',
75
+ });
76
+ ```
77
+
78
+ Discover what any service offers:
79
+
80
+ ```js
81
+ const tools = await orb().listTools();
82
+ console.log(tools.map((t) => t.name).join(', '));
83
+ ```
84
+
85
+ ## CLI
86
+
87
+ ```bash
88
+ export TUNC_API_KEY=tunc_...
89
+
90
+ npx tunc-mcp services
91
+ npx tunc-mcp tools --service orb
92
+ npx tunc-mcp call data_read '{"name":"contacts/ada"}' --service orb
93
+ ```
94
+
95
+ ## Wire it into an MCP client
96
+
97
+ Any MCP-capable host works — Claude Desktop, Cursor, Claude Code, Codex, a custom
98
+ orchestrator. Point it at the endpoint and pass the key as a bearer header:
99
+
100
+ ```json
101
+ {
102
+ "mcpServers": {
103
+ "tunc-orb": {
104
+ "type": "http",
105
+ "url": "https://mcp.tuncorb.com/mcp",
106
+ "headers": { "Authorization": "Bearer ${TUNC_API_KEY}" }
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ ## Agent-to-agent
113
+
114
+ Every service publishes an A2A agent card, so agents that speak A2A can discover
115
+ and call them without a human in the loop:
116
+
117
+ - <https://tuncorb.com/.well-known/agent-card.json>
118
+ - <https://tuncart.com/.well-known/agent-card.json>
119
+ - <https://tuncdoc.com/.well-known/agent-card.json>
120
+
121
+ ## What this package does not do
122
+
123
+ It is a client. It does not host anything, and it does not bundle an agent
124
+ runtime. If you want the workspace these services belong to — a multiplayer
125
+ chat where people and agents work together — that is a desktop and mobile app:
126
+ <https://tunchub.com>.
127
+
128
+ ## Errors
129
+
130
+ Everything throws `TuncMcpError` with a `status`, a `code` and the server's
131
+ message. A `401` means the key is missing, revoked, or not scoped for that
132
+ service; the message says which.
133
+
134
+ ## 中文
135
+
136
+ **面向 AI 智能体的记忆数据库、制品存储与文档编辑 —— 通过 MCP 接入。**
137
+
138
+ `tunc-mcp` 是一个零依赖的 MCP 客户端,连接三个托管服务,解决多智能体系统里最常见的问题:
139
+
140
+ | 服务 | 用途 | 接入点 |
141
+ |---|---|---|
142
+ | **Tunc Orb** | **智能体记忆数据库** —— 持久保存记录、日志与结构化状态,可在所有智能体之间共享 | `https://mcp.tuncorb.com/mcp` |
143
+ | **Tunc Art** | **制品存储与文件分享** —— 保存智能体生成的文件,返回私密短链接 | `https://mcp.tuncart.com/mcp` |
144
+ | **Tunc Doc** | **文档、表格与幻灯片** —— 可导入导出 DOCX、XLSX、PPTX、PDF | `https://mcp.tuncdoc.com/mcp` |
145
+
146
+ 三个服务同时支持 **MCP**、**A2A**(智能体间通信)与 **REST**,同一个账号即可通用。
147
+
148
+ ### 为什么需要它
149
+
150
+ 多数智能体框架在上下文窗口滚动之后就会丢失状态;两个智能体同时写入会互相覆盖;生成的制品散落在本地磁盘上。这里提供三个朴素可靠的基础能力:
151
+
152
+ - **能跨越对话的记忆。** 记录跨会话、跨智能体、跨模型持久保存,写入支持 `if_revision` 乐观并发控制。
153
+ - **可分享的制品链接。** 上传文件即得到私密短链接,无需配置对象存储。
154
+ - **智能体能够真正产出的文档。** 真实的 DOCX、XLSX 与 PPTX,而不是伪装成报告的 Markdown。
155
+
156
+ ### 并发写入
157
+
158
+ `data_update` 接受 `if_revision`。两个智能体写同一条记录时不会互相覆盖:后到的写入收到 `revision_conflict`,重新读取后重试。这把多智能体最常见的“静默丢更新”故障,变成一个可以被发现的错误。
159
+
160
+ ```bash
161
+ npm install tunc-mcp
162
+ ```
163
+
164
+ API 密钥:<https://tunchub.com/account/keys/new>(可设为只读)。
165
+
166
+ 完整的多人协作工作空间(人与多个智能体共享同一实时对话)见 <https://tunchub.com>。
167
+
168
+ ## License
169
+
170
+ MIT © TUNC LLC
package/bin/cli.js ADDED
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ // tunc-mcp — inspect and call the Tunc MCP servers from a terminal.
3
+ //
4
+ // export TUNC_API_KEY=tunc_...
5
+ // npx tunc-mcp tools --service orb
6
+ // npx tunc-mcp call data_read '{"name":"contacts/ada"}'
7
+
8
+ import { SERVERS, TuncMcpClient, TuncMcpError } from '../src/client.js';
9
+
10
+ const USAGE = `tunc-mcp — MCP client for Tunc Orb, Tunc Art and Tunc Doc
11
+
12
+ Usage:
13
+ tunc-mcp services
14
+ tunc-mcp tools [--service orb|art|doc] [--json]
15
+ tunc-mcp call <tool> [json-args] [--service orb|art|doc]
16
+
17
+ Environment:
18
+ TUNC_API_KEY required. Create one at https://tunchub.com/account/keys/new
19
+ `;
20
+
21
+ function parseArgs(argv) {
22
+ const args = { _: [], service: 'orb', json: false };
23
+ for (let i = 0; i < argv.length; i += 1) {
24
+ const arg = argv[i];
25
+ if (arg === '--service') args.service = argv[++i];
26
+ else if (arg === '--json') args.json = true;
27
+ else if (arg === '--help' || arg === '-h') args.help = true;
28
+ else args._.push(arg);
29
+ }
30
+ return args;
31
+ }
32
+
33
+ async function main() {
34
+ const args = parseArgs(process.argv.slice(2));
35
+ const [command, ...rest] = args._;
36
+
37
+ if (args.help || !command || command === 'services') {
38
+ if (command === 'services') {
39
+ console.log(JSON.stringify(SERVERS, null, 2));
40
+ return;
41
+ }
42
+ console.log(USAGE);
43
+ return;
44
+ }
45
+
46
+ const client = new TuncMcpClient({ service: args.service });
47
+
48
+ if (command === 'tools') {
49
+ const tools = await client.listTools();
50
+ if (args.json) {
51
+ console.log(JSON.stringify(tools, null, 2));
52
+ return;
53
+ }
54
+ console.log(`${tools.length} tools on ${args.service} (${client.endpoint})\n`);
55
+ for (const tool of tools) {
56
+ console.log(` ${tool.name}${tool.description ? ` — ${tool.description.split('.')[0]}` : ''}`);
57
+ }
58
+ return;
59
+ }
60
+
61
+ if (command === 'call') {
62
+ const [name, rawArgs] = rest;
63
+ if (!name) {
64
+ console.error('call needs a tool name. Run: tunc-mcp tools');
65
+ process.exitCode = 1;
66
+ return;
67
+ }
68
+ let parsed = {};
69
+ if (rawArgs) {
70
+ try {
71
+ parsed = JSON.parse(rawArgs);
72
+ } catch {
73
+ console.error('That is not valid JSON. Quote it, e.g. \'{"name":"x"}\'');
74
+ process.exitCode = 1;
75
+ return;
76
+ }
77
+ }
78
+ const result = await client.callTool(name, parsed);
79
+ console.log(JSON.stringify(result, null, 2));
80
+ return;
81
+ }
82
+
83
+ console.error(`Unknown command: ${command}\n`);
84
+ console.log(USAGE);
85
+ process.exitCode = 1;
86
+ }
87
+
88
+ main().catch((error) => {
89
+ if (error instanceof TuncMcpError) {
90
+ console.error(`tunc-mcp: ${error.message}`);
91
+ if (error.status) console.error(` HTTP ${error.status}`);
92
+ } else {
93
+ console.error(`tunc-mcp: ${error.message}`);
94
+ }
95
+ process.exitCode = 1;
96
+ });
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "tunc-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "MCP client for Tunc Orb (agent memory database), Tunc Art (artifact storage and private file sharing) and Tunc Doc (documents, spreadsheets, decks).",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "agent-memory",
9
+ "agent-database",
10
+ "memory",
11
+ "agent",
12
+ "agents",
13
+ "multi-agent",
14
+ "multi-agent-orchestration",
15
+ "artifact",
16
+ "artifacts",
17
+ "artifact-storage",
18
+ "file-sharing",
19
+ "private-links",
20
+ "storage",
21
+ "database",
22
+ "records",
23
+ "documents",
24
+ "spreadsheets",
25
+ "slide-decks",
26
+ "a2a",
27
+ "agent-to-agent",
28
+ "ai-agents",
29
+ "llm",
30
+ "tunc"
31
+ ],
32
+ "type": "module",
33
+ "main": "./src/index.js",
34
+ "exports": {
35
+ ".": "./src/index.js",
36
+ "./client": "./src/client.js"
37
+ },
38
+ "bin": {
39
+ "tunc-mcp": "./bin/cli.js"
40
+ },
41
+ "files": [
42
+ "src",
43
+ "bin",
44
+ "README.md"
45
+ ],
46
+ "engines": {
47
+ "node": ">=18"
48
+ },
49
+ "scripts": {
50
+ "test": "node --test"
51
+ },
52
+ "license": "MIT",
53
+ "homepage": "https://tuncorb.com",
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "https://github.com/TUNC-AI/tunc-mcp.git"
57
+ }
58
+ }
package/src/client.js ADDED
@@ -0,0 +1,177 @@
1
+ // Minimal MCP (Model Context Protocol) client over streamable HTTP.
2
+ //
3
+ // It speaks just enough of the protocol to talk to the Tunc servers: initialize,
4
+ // tools/list and tools/call, with JSON-RPC 2.0 envelopes and event-stream
5
+ // responses. No dependencies; Node 18+ fetch only.
6
+ //
7
+ // Servers:
8
+ // Tunc Orb https://mcp.tuncorb.com/mcp agent memory database
9
+ // Tunc Art https://mcp.tuncart.com/mcp artifact storage, private links
10
+ // Tunc Doc https://mcp.tuncdoc.com/mcp documents, spreadsheets, decks
11
+
12
+ export const SERVERS = Object.freeze({
13
+ orb: 'https://mcp.tuncorb.com/mcp',
14
+ art: 'https://mcp.tuncart.com/mcp',
15
+ doc: 'https://mcp.tuncdoc.com/mcp',
16
+ });
17
+
18
+ export class TuncMcpError extends Error {
19
+ constructor(message, { code, status, body } = {}) {
20
+ super(message);
21
+ this.name = 'TuncMcpError';
22
+ this.code = code;
23
+ this.status = status;
24
+ this.body = body;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Pull the JSON-RPC payload out of an event-stream body, or a plain JSON body.
30
+ * Streamable HTTP lets a server answer either way; Tunc answers JSON, other
31
+ * MCP servers may answer text/event-stream, so both are handled.
32
+ */
33
+ export function parseBody(contentType, text) {
34
+ if (!text) return null;
35
+ if (!/text\/event-stream/i.test(contentType ?? '')) {
36
+ try {
37
+ return JSON.parse(text);
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ let last = null;
44
+ for (const block of text.split(/\r?\n\r?\n/)) {
45
+ const data = block
46
+ .split(/\r?\n/)
47
+ .filter((line) => line.startsWith('data:'))
48
+ .map((line) => line.slice(5).trimStart())
49
+ .join('\n');
50
+ if (!data) continue;
51
+ try {
52
+ last = JSON.parse(data);
53
+ } catch {
54
+ // A partial frame: keep the previous complete one.
55
+ }
56
+ }
57
+ return last;
58
+ }
59
+
60
+ export class TuncMcpClient {
61
+ #endpoint;
62
+ #apiKey;
63
+ #sessionId = null;
64
+ #nextId = 1;
65
+ #initialized = false;
66
+
67
+ constructor({ service = 'orb', endpoint, apiKey }) {
68
+ this.#endpoint = endpoint ?? SERVERS[service];
69
+ if (!this.#endpoint) {
70
+ throw new TuncMcpError(`Unknown service "${service}". Use orb, art or doc, or pass an endpoint.`);
71
+ }
72
+ this.#apiKey = apiKey ?? process.env.TUNC_API_KEY;
73
+ if (!this.#apiKey) {
74
+ throw new TuncMcpError(
75
+ 'No API key. Pass apiKey or set TUNC_API_KEY. Create one at https://tunchub.com/account/keys/new',
76
+ );
77
+ }
78
+ }
79
+
80
+ get endpoint() {
81
+ return this.#endpoint;
82
+ }
83
+
84
+ async #post(payload) {
85
+ const headers = {
86
+ 'content-type': 'application/json',
87
+ accept: 'application/json, text/event-stream',
88
+ authorization: `Bearer ${this.#apiKey}`,
89
+ };
90
+ if (this.#sessionId) headers['mcp-session-id'] = this.#sessionId;
91
+
92
+ const response = await fetch(this.#endpoint, {
93
+ method: 'POST',
94
+ headers,
95
+ body: JSON.stringify(payload),
96
+ });
97
+
98
+ const session = response.headers.get('mcp-session-id');
99
+ if (session) this.#sessionId = session;
100
+
101
+ const text = await response.text();
102
+
103
+ if (response.status === 401) {
104
+ throw new TuncMcpError(
105
+ 'Unauthorized. The API key is missing, revoked, or not scoped for this service.',
106
+ { status: 401, body: text },
107
+ );
108
+ }
109
+
110
+ const body = parseBody(response.headers.get('content-type'), text);
111
+
112
+ if (!response.ok) {
113
+ throw new TuncMcpError(body?.error?.message ?? `HTTP ${response.status}`, {
114
+ status: response.status,
115
+ code: body?.error?.code,
116
+ body,
117
+ });
118
+ }
119
+
120
+ if (body?.error) {
121
+ throw new TuncMcpError(body.error.message ?? 'JSON-RPC error', {
122
+ code: body.error.code,
123
+ status: response.status,
124
+ body,
125
+ });
126
+ }
127
+
128
+ return body?.result;
129
+ }
130
+
131
+ async #rpc(method, params) {
132
+ return this.#post({ jsonrpc: '2.0', id: this.#nextId++, method, params });
133
+ }
134
+
135
+ async initialize() {
136
+ if (this.#initialized) return this;
137
+ const result = await this.#rpc('initialize', {
138
+ protocolVersion: '2025-06-18',
139
+ capabilities: {},
140
+ clientInfo: { name: 'tunc-mcp', version: '0.1.0' },
141
+ });
142
+ // Notifications carry no id and expect no body.
143
+ await fetch(this.#endpoint, {
144
+ method: 'POST',
145
+ headers: {
146
+ 'content-type': 'application/json',
147
+ accept: 'application/json, text/event-stream',
148
+ authorization: `Bearer ${this.#apiKey}`,
149
+ ...(this.#sessionId ? { 'mcp-session-id': this.#sessionId } : {}),
150
+ },
151
+ body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }),
152
+ }).catch(() => {});
153
+ this.serverInfo = result?.serverInfo ?? null;
154
+ this.capabilities = result?.capabilities ?? null;
155
+ this.#initialized = true;
156
+ return this;
157
+ }
158
+
159
+ async listTools() {
160
+ await this.initialize();
161
+ const result = await this.#rpc('tools/list', {});
162
+ return result?.tools ?? [];
163
+ }
164
+
165
+ async callTool(name, args = {}) {
166
+ await this.initialize();
167
+ return this.#rpc('tools/call', { name, arguments: args });
168
+ }
169
+
170
+ /** Convenience: the text content of a tool result, joined. */
171
+ async callText(name, args = {}) {
172
+ const result = await this.callTool(name, args);
173
+ const parts = Array.isArray(result?.content) ? result.content : [];
174
+ const text = parts.filter((p) => p?.type === 'text').map((p) => p.text).join('\n');
175
+ return text || result;
176
+ }
177
+ }
package/src/index.js ADDED
@@ -0,0 +1,33 @@
1
+ // Convenience layer over the three Tunc MCP servers.
2
+ //
3
+ // Tunc Orb — agent memory database: durable records and logs for agents and
4
+ // agent swarms, revision-guarded so concurrent writers cannot clobber.
5
+ // Tunc Art — artifact storage and private file sharing: agent-generated files
6
+ // with short private links.
7
+ // Tunc Doc — documents, spreadsheets and slide decks, import/export DOCX,
8
+ // XLSX, PPTX and PDF.
9
+
10
+ import { SERVERS, TuncMcpClient, TuncMcpError, parseBody } from './client.js';
11
+
12
+ export { SERVERS, TuncMcpClient, TuncMcpError, parseBody };
13
+
14
+ export function orb(options = {}) {
15
+ return new TuncMcpClient({ service: 'orb', ...options });
16
+ }
17
+
18
+ export function art(options = {}) {
19
+ return new TuncMcpClient({ service: 'art', ...options });
20
+ }
21
+
22
+ export function doc(options = {}) {
23
+ return new TuncMcpClient({ service: 'doc', ...options });
24
+ }
25
+
26
+ /** All three clients from one key. */
27
+ export function connect(apiKey = process.env.TUNC_API_KEY) {
28
+ return {
29
+ orb: orb({ apiKey }),
30
+ art: art({ apiKey }),
31
+ doc: doc({ apiKey }),
32
+ };
33
+ }