@commonlyai/mcp 0.1.6 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/mcp",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Commonly MCP Server \u2014 exposes the kernel HTTP surface (CAP per ADR-004) as standard MCP tools so any MCP-capable runtime can consume `commonly_*` tools without driver-specific code.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/client.js CHANGED
@@ -98,3 +98,42 @@ export const request = async (config, { method, path, query, body, _fetchImpl =
98
98
  }
99
99
  return parsed;
100
100
  };
101
+
102
+ /**
103
+ * Multipart upload — for attaching a local file to a pod. Node 18+ has global
104
+ * FormData / Blob, so no runtime deps. `fileField` is the multipart field name
105
+ * the backend's multer middleware expects. Same response handling as `request`.
106
+ */
107
+ export const requestUpload = async (config, {
108
+ path, fileBuffer, fileName, contentType, fileField = 'file', fields = {}, _fetchImpl = fetch,
109
+ } = {}) => {
110
+ const url = `${config.baseUrl}${path}`;
111
+ const form = new FormData();
112
+ form.append(
113
+ fileField,
114
+ new Blob([fileBuffer], { type: contentType || 'application/octet-stream' }),
115
+ fileName,
116
+ );
117
+ for (const [k, v] of Object.entries(fields)) {
118
+ if (v !== undefined && v !== null) form.append(k, String(v));
119
+ }
120
+ // Do NOT set Content-Type — fetch sets the multipart boundary itself.
121
+ const res = await _fetchImpl(url, {
122
+ method: 'POST',
123
+ headers: {
124
+ Authorization: `Bearer ${config.token}`,
125
+ 'User-Agent': USER_AGENT,
126
+ Accept: 'application/json',
127
+ },
128
+ body: form,
129
+ });
130
+ const raw = await res.text();
131
+ let parsed;
132
+ try { parsed = raw ? JSON.parse(raw) : null; } catch { parsed = raw; }
133
+ if (!res.ok) {
134
+ const message = (parsed && typeof parsed === 'object' && parsed.message)
135
+ || (typeof parsed === 'string' ? parsed.slice(0, 500) : `HTTP ${res.status}`);
136
+ throw new HttpError(res.status, parsed, message);
137
+ }
138
+ return parsed;
139
+ };
package/src/tools.js CHANGED
@@ -18,7 +18,21 @@
18
18
  * human-JWT-only route.
19
19
  */
20
20
 
21
- import { request, HttpError } from './client.js';
21
+ import { readFileSync } from 'fs';
22
+ import { basename, extname } from 'path';
23
+ import { request, requestUpload, HttpError } from './client.js';
24
+
25
+ // Minimal content-type guess for attach — the backend re-derives `kind`, this
26
+ // just sets a sensible multipart type. Keep the map small; default is generic.
27
+ const CONTENT_TYPES = {
28
+ '.txt': 'text/plain', '.md': 'text/markdown', '.csv': 'text/csv',
29
+ '.json': 'application/json', '.pdf': 'application/pdf', '.png': 'image/png',
30
+ '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
31
+ '.html': 'text/html', '.xml': 'application/xml', '.log': 'text/plain',
32
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
33
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
34
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
35
+ };
22
36
 
23
37
  // Convert a successful response into the MCP `content` shape. Strings and
24
38
  // JSON-serialisable values both go through `JSON.stringify` so the model
@@ -117,6 +131,35 @@ export const buildTools = (config) => {
117
131
  path: `/api/agents/runtime/pods/${encodeURIComponent(podId)}/files/${encodeURIComponent(fileName)}/content`,
118
132
  })),
119
133
  },
134
+ {
135
+ name: 'commonly_attach_file',
136
+ description: 'Attach a file from THIS machine into a pod so humans and other agents can see and read it — a report you wrote, a diff, a generated CSV/deck, etc. Pass `filePath` (a path on the local filesystem). It uploads the file and posts it into the pod as a file card; add `message` for a line of context alongside it. Returns the uploaded file metadata (its `fileName` is what commonly_read_file takes).',
137
+ inputSchema: reqWith({ podId: STRING, filePath: STRING, message: STRING }, ['podId', 'filePath']),
138
+ call: wrap(async ({ podId, filePath, message }) => {
139
+ const buffer = readFileSync(filePath);
140
+ const name = basename(filePath);
141
+ const contentType = CONTENT_TYPES[extname(name).toLowerCase()] || 'application/octet-stream';
142
+ const uploaded = await requestUpload(config, {
143
+ path: `/api/agents/runtime/pods/${encodeURIComponent(podId)}/uploads`,
144
+ fileBuffer: buffer,
145
+ fileName: name,
146
+ contentType,
147
+ fileField: 'file',
148
+ fields: { podId },
149
+ });
150
+ // Post a message with the [[upload:…]] directive so it renders as a
151
+ // file pill in the thread (same shape the human composer emits).
152
+ const u = uploaded || {};
153
+ const directive = `[[upload:${u.fileName || name}|${u.originalName || name}|${u.size || buffer.length}|${u.kind || 'document'}]]`;
154
+ const content = message ? `${message} ${directive}` : directive;
155
+ await request(config, {
156
+ method: 'POST',
157
+ path: `/api/agents/runtime/pods/${encodeURIComponent(podId)}/messages`,
158
+ body: { content },
159
+ });
160
+ return uploaded;
161
+ }),
162
+ },
120
163
  {
121
164
  name: 'commonly_get_posts',
122
165
  description: 'List recent posts in a pod. Each post includes recent human comments (full text, last 5) and recent agent comments (60-char preview, last 3).',