@kammcs/buildit-mcp 0.10.0 → 0.11.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.
@@ -8,18 +8,27 @@
8
8
  * latest message, mention or assignment that arrived, else as a new
9
9
  * message of the direct conversation. It is safe to retry: the
10
10
  * idempotency key is the new message's id.
11
+ *
12
+ * With `request` (agent API 1.13.0) the post is the result of a request
13
+ * from buildIt.Social (a <channel> tag with event="request"). Without
14
+ * thread_id or item it starts a new message of the direct conversation,
15
+ * never "the latest thread" (which may be an item's): a catch-up's result
16
+ * is private. A request this session knows goes only where its `result`
17
+ * says; the server refuses anything else too (request_result_private).
11
18
  */
12
19
  import { randomUUID } from 'node:crypto';
13
20
  import { rm } from 'node:fs/promises';
14
21
  import { z } from 'zod';
15
22
  import { postSessionMessage } from '../channel/api.js';
23
+ import { REQUESTS_API } from '../channel/requests.js';
16
24
  import { FILES_API, FILES_OPERATIONS, filesServed } from '../channel/attachments.js';
17
25
  import { checkFiles, prepareFiles, REPLY_MAX_FILES, shareFiles } from '../channel/upload.js';
18
26
  import { NOT_THE_CHANNEL } from '../channel/lock.js';
19
- import { isUuid, resolveThread } from '../channel/threads.js';
27
+ import { DM_AND_THREAD, dmThread, isItemThread, isUuid, resolveThread, } from '../channel/threads.js';
20
28
  import { ToolInputError } from '../errors.js';
21
29
  import { defineTool } from '../toolsets/registry.js';
22
30
  import { sanitizeLabel } from '../untrusted.js';
31
+ import { compareVersions } from '../version.js';
23
32
  import { IdempotencyKeyInput, itemRef, ItemRefInput } from './shared.js';
24
33
  export const REPLY_MAX_CHARS = 4000;
25
34
  const TextInput = z
@@ -36,6 +45,14 @@ const ReplyToInput = z
36
45
  .uuid()
37
46
  .optional()
38
47
  .describe('A message_id to quote as the one you answer, inside the thread.');
48
+ const DmInput = z
49
+ .boolean()
50
+ .optional()
51
+ .describe('true: post in your direct message with the person (its latest thread, or a new message), never in an item thread. Use it for anything about a request (event="request") other than its result: a note, a queued notice.');
52
+ const RequestInput = z
53
+ .uuid()
54
+ .optional()
55
+ .describe('The request_id of a <channel> tag with event="request", when this post is its result. A request whose result is "dm" (a catch-up) goes only to your direct message: leave thread_id and item out.');
39
56
  /**
40
57
  * reply on a server older than 1.11.0: loose, so a call that still passes
41
58
  * `files` (from a tool list read before the server's version was known)
@@ -46,6 +63,8 @@ export const ReplyInputSchema = z.looseObject({
46
63
  thread_id: ThreadInput,
47
64
  item: ItemInput,
48
65
  reply_to_id: ReplyToInput,
66
+ dm: DmInput,
67
+ request: RequestInput,
49
68
  idempotency_key: IdempotencyKeyInput.optional(),
50
69
  });
51
70
  /** reply on a server that serves files: `files`, and text optional with them. */
@@ -60,6 +79,8 @@ export const ReplyWithFilesInputSchema = z.object({
60
79
  thread_id: ThreadInput,
61
80
  item: ItemInput,
62
81
  reply_to_id: ReplyToInput,
82
+ dm: DmInput,
83
+ request: RequestInput,
63
84
  idempotency_key: IdempotencyKeyInput.optional(),
64
85
  });
65
86
  const MessageOutput = z.object({
@@ -90,6 +111,7 @@ const ReplyWithFilesOutputSchema = ReplyOutputSchema.extend({
90
111
  .describe('The files posted, as given, with the name readers see.'),
91
112
  });
92
113
  const BASE_DESCRIPTION = `Answers the person who directs this session, in buildIt.Social: posts your text as the agent (labelled with this session) in its conversation with them, or in a work item's thread when the message came from one (meta item), where they read it on their computer or phone. Use it for every answer to a message that arrived as a <channel> tag: pass that tag's thread_id so the answer lands in the same thread (left out, it goes to the thread of the latest message, mention or assignment; with none yet, it starts a new message in your conversation with them). For an item assigned to you, post progress, questions and the result in its thread: pass its thread_id, or item with its key. You can post only in the threads of items you work on; everyone in the project's channel reads them. Markdown, at most ${REPLY_MAX_CHARS} characters; split longer answers. idempotency_key makes a retry return the first message instead of posting twice; one is generated and returned if you leave it out.`;
114
+ const REQUEST_DESCRIPTION = ` For the result of a request (a <channel> tag with event="request"), pass its request_id as request, and post where the tag's result says: result="dm" means your direct message only, so leave thread_id and item out. Anything else about a request (a note, a queued notice) goes with dm: true, so it never lands in an item's thread.`;
93
115
  const FILES_DESCRIPTION = ` To share files (a render, a screenshot, a log, a document), pass their local paths in files: they are posted at once in the same thread, with the text, and everyone who reads the thread can open them. Before anything is uploaded, every file is checked: it must be inside the project folder or the temp folder, not a secret store (.env files, keys, certificates, .ssh and the like), not a program, within the size limit (250 MiB for a video, 100 MiB for anything else), and a text file must not hold anything that looks like a credential. Location is removed from photos and videos. One refused file sends nothing, and the error says why: fix it and send the reply again. Share only files the person asked for or that your work needs.`;
94
116
  /** The refusal of files on a server that doesn't take them: nothing is sent. */
95
117
  const TOO_OLD_FOR_FILES = `Not sent: this buildIt.Social server is older than agent API ${FILES_API}; files can't be sent. Nothing was posted. Send the text alone, and describe the file (or where it is) instead.`;
@@ -109,6 +131,48 @@ function whereOf(m, item) {
109
131
  : `in thread ${m.thread_id}`
110
132
  : 'as a new message';
111
133
  }
134
+ /** The refusal of a private result posted outside the direct message, before any call. */
135
+ const PRIVATE_RESULT = "Not sent (request_result_private): this request's result goes only to your direct message with the person; a catch-up is private. Send it again with reply, without thread_id or item, and with the same request.";
136
+ /**
137
+ * Where reply posts. With `request` and no thread named: a new message of
138
+ * the direct conversation (never the latest thread, which may be an
139
+ * item's). A request this session knows with result "dm" is refused in an
140
+ * item's thread here, before any call (the server refuses it too).
141
+ */
142
+ async function replyThread(state, ctx, args) {
143
+ if (args.dm === true) {
144
+ if (args.thread_id !== undefined || args.item !== undefined) {
145
+ throw new ToolInputError(DM_AND_THREAD);
146
+ }
147
+ return dmThread(state);
148
+ }
149
+ if (args.request === undefined)
150
+ return resolveThread(state, ctx, args, 'reply');
151
+ const known = state.requests.get(args.request.toLowerCase());
152
+ if (known?.place === 'dm') {
153
+ if (args.item !== undefined)
154
+ throw new ToolInputError(PRIVATE_RESULT);
155
+ if (args.thread_id !== undefined && isItemThread(state, args.thread_id)) {
156
+ throw new ToolInputError(PRIVATE_RESULT);
157
+ }
158
+ }
159
+ if (args.thread_id === undefined && args.item === undefined)
160
+ return undefined;
161
+ return resolveThread(state, ctx, args, 'reply');
162
+ }
163
+ /**
164
+ * The body's `request`, on a server that takes it (agent API 1.13.0 and
165
+ * newer, or one that hasn't said its version). An older server never sent
166
+ * a request, so a post there is an ordinary one.
167
+ */
168
+ function requestField(ctx, request) {
169
+ if (request === undefined)
170
+ return {};
171
+ const version = ctx.api.served?.apiVersion;
172
+ if (version !== undefined && compareVersions(version, REQUESTS_API) < 0)
173
+ return {};
174
+ return { request };
175
+ }
112
176
  /**
113
177
  * The reply tool for one channel session. With `files` (a server that
114
178
  * serves them, 1.11.0 and newer), it also posts local files; without, it
@@ -128,7 +192,7 @@ export function createReplyTool(state, options = { files: false }) {
128
192
  // Not in the catalog: listed only in channel mode, whatever the toolsets.
129
193
  toolset: 'chat',
130
194
  title: 'Reply in buildIt.Social',
131
- description: BASE_DESCRIPTION,
195
+ description: BASE_DESCRIPTION + REQUEST_DESCRIPTION,
132
196
  scopes: [],
133
197
  // Listed only for a server known not to serve files (the other reply is the default).
134
198
  listedUnlessOperations: FILES_OPERATIONS,
@@ -142,12 +206,13 @@ export function createReplyTool(state, options = { files: false }) {
142
206
  throw new ToolInputError(TOO_OLD_FOR_FILES);
143
207
  }
144
208
  const key = args.idempotency_key ?? randomUUID();
145
- const threadId = await resolveThread(state, ctx, args, 'reply');
209
+ const threadId = await replyThread(state, ctx, args);
146
210
  const r = await postSessionMessage(ctx.api, {
147
211
  id: key,
148
212
  text: args.text,
149
213
  ...(threadId === undefined ? {} : { thread_id: threadId }),
150
214
  ...(args.reply_to_id === undefined ? {} : { reply_to_id: args.reply_to_id }),
215
+ ...requestField(ctx, args.request),
151
216
  }, ctx.apiOptions);
152
217
  state.typing?.stop(threadId);
153
218
  const m = r.message;
@@ -166,7 +231,7 @@ export function createReplyTool(state, options = { files: false }) {
166
231
  name: 'reply',
167
232
  toolset: 'chat',
168
233
  title: 'Reply in buildIt.Social',
169
- description: BASE_DESCRIPTION + FILES_DESCRIPTION,
234
+ description: BASE_DESCRIPTION + REQUEST_DESCRIPTION + FILES_DESCRIPTION,
170
235
  scopes: [],
171
236
  requiresOperations: FILES_OPERATIONS,
172
237
  annotations,
@@ -183,11 +248,12 @@ export function createReplyTool(state, options = { files: false }) {
183
248
  throw new ToolInputError(TOO_OLD_FOR_FILES);
184
249
  }
185
250
  const key = args.idempotency_key ?? randomUUID();
186
- const threadId = await resolveThread(state, ctx, args, 'reply');
251
+ const threadId = await replyThread(state, ctx, args);
187
252
  const item = args.item === undefined ? undefined : itemRef(args.item).toUpperCase();
188
253
  const base = {
189
254
  ...(args.text === undefined ? {} : { text: args.text }),
190
255
  ...(args.reply_to_id === undefined ? {} : { reply_to_id: args.reply_to_id }),
256
+ ...requestField(ctx, args.request),
191
257
  };
192
258
  if (files.length === 0) {
193
259
  const r = await postSessionMessage(ctx.api, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kammcs/buildit-mcp",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "MCP server for buildIt.Social: lets AI agents work with an org's projects, pages and channels through the buildIt.Social agent API, with a personal access token.",
5
5
  "license": "MIT",
6
6
  "author": "Kamm Creative Solutions",