@kammcs/buildit-mcp 0.2.0 → 0.7.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/CHANGELOG.md +116 -0
- package/README.md +252 -32
- package/SECURITY.md +1 -1
- package/dist/api/client.js +5 -1
- package/dist/api/generated/operations.js +661 -4
- package/dist/api/generated/schemas.js +680 -2
- package/dist/api/generated/strict.js +799 -2
- package/dist/channel/api.js +128 -0
- package/dist/channel/approvals.js +259 -0
- package/dist/channel/item-events.js +138 -0
- package/dist/channel/lock.js +181 -0
- package/dist/channel/loop.js +776 -0
- package/dist/channel/mask.js +365 -0
- package/dist/channel/threads.js +60 -0
- package/dist/channel/tool-ring.js +124 -0
- package/dist/channel/typing.js +150 -0
- package/dist/channel/waiters.js +229 -0
- package/dist/channel/workers.js +66 -0
- package/dist/cli/claude-config.js +100 -0
- package/dist/cli/commands.js +410 -0
- package/dist/config.js +329 -6
- package/dist/errors.js +38 -0
- package/dist/index.js +122 -6
- package/dist/log.js +3 -2
- package/dist/secrets/keychain.js +273 -0
- package/dist/server.js +53 -5
- package/dist/tools/approve.js +791 -0
- package/dist/tools/ask.js +898 -0
- package/dist/tools/chat.js +98 -1
- package/dist/tools/items-read.js +5 -3
- package/dist/tools/items-write.js +66 -15
- package/dist/tools/plans.js +221 -14
- package/dist/tools/reply.js +138 -0
- package/dist/tools/shared.js +10 -1
- package/dist/tools/threads.js +123 -0
- package/dist/tools/whoami.js +17 -3
- package/dist/toolsets/catalog.js +3 -1
- package/dist/toolsets/registry.js +2 -0
- package/dist/transports/stdio.js +260 -6
- package/package.json +5 -1
package/dist/tools/whoami.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* whoami: who the token acts as, where, and with what reach.
|
|
3
3
|
*/
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { NOT_THE_CHANNEL } from '../channel/lock.js';
|
|
5
6
|
import { OPERATIONS } from '../api/generated/operations.js';
|
|
6
7
|
import { defineTool } from '../toolsets/registry.js';
|
|
7
8
|
import { expandScopes } from '../toolsets/toolsets.js';
|
|
@@ -24,7 +25,14 @@ const OutputSchema = z.object({
|
|
|
24
25
|
.describe('A test org (true); null when the API is older than 1.2.0.'),
|
|
25
26
|
}),
|
|
26
27
|
token: z.object({
|
|
27
|
-
name: z
|
|
28
|
+
name: z
|
|
29
|
+
.string()
|
|
30
|
+
.nullable()
|
|
31
|
+
.describe("The token's name; for an agent session, the session's label."),
|
|
32
|
+
kind: z
|
|
33
|
+
.string()
|
|
34
|
+
.nullable()
|
|
35
|
+
.describe('pat: a personal access token; session: an agent session (channel mode); null when the API is older.'),
|
|
28
36
|
expires_at: z.string().nullable(),
|
|
29
37
|
}),
|
|
30
38
|
scopes: z.array(z.string()).describe('As granted; a write scope also allows reading.'),
|
|
@@ -137,6 +145,7 @@ export const whoamiTool = defineTool({
|
|
|
137
145
|
},
|
|
138
146
|
token: {
|
|
139
147
|
name: nullableLabel(me.token.name, NAME_CHARS),
|
|
148
|
+
kind: me.token.kind === undefined ? null : sanitizeLabel(me.token.kind, 20),
|
|
140
149
|
expires_at: me.token.expires_at,
|
|
141
150
|
},
|
|
142
151
|
scopes: [...me.token.scopes].sort(),
|
|
@@ -176,7 +185,9 @@ export const whoamiTool = defineTool({
|
|
|
176
185
|
const names = [
|
|
177
186
|
`User: ${s.user.name ?? s.user.id}${s.user.email ? ` (${s.user.email})` : ''}`,
|
|
178
187
|
`Org: ${s.org.name ?? s.org.id}`,
|
|
179
|
-
...(s.token.name
|
|
188
|
+
...(s.token.name
|
|
189
|
+
? [`${s.token.kind === 'session' ? 'Session' : 'Token'}: ${s.token.name}`]
|
|
190
|
+
: []),
|
|
180
191
|
];
|
|
181
192
|
if (s.projects.length > 0) {
|
|
182
193
|
const shown = s.projects
|
|
@@ -188,7 +199,7 @@ export const whoamiTool = defineTool({
|
|
|
188
199
|
names.push(`Channels the token is limited to: ${s.limits.channels.join(', ')}`);
|
|
189
200
|
}
|
|
190
201
|
const lines = [
|
|
191
|
-
`Signed in to buildIt.Social with a personal access token${s.token.expires_at ? ` (expires ${s.token.expires_at})` : ''}. Who and where, as people named them:`,
|
|
202
|
+
`Signed in to buildIt.Social with ${s.token.kind === 'session' ? "an agent session's token (it acts as the session's owner)" : 'a personal access token'}${s.token.expires_at ? ` (expires ${s.token.expires_at})` : ''}. Who and where, as people named them:`,
|
|
192
203
|
wrapLines(names, 'names'),
|
|
193
204
|
`Scopes: ${s.scopes.length > 0 ? s.scopes.join(', ') : 'none'}.`,
|
|
194
205
|
`Limits: ${s.limits.projects === null
|
|
@@ -222,6 +233,9 @@ export const whoamiTool = defineTool({
|
|
|
222
233
|
if (s.server.not_served !== null && s.server.not_served.length > 0) {
|
|
223
234
|
lines.push(`This buildIt.Social server (API ${s.server.api_version ?? 'unknown'}) doesn't serve these operations yet, so calls that need them are refused until it is updated: ${s.server.not_served.join(', ')}.`);
|
|
224
235
|
}
|
|
236
|
+
if (ctx.channel !== undefined && !ctx.channel.isChannel) {
|
|
237
|
+
lines.push(NOT_THE_CHANNEL);
|
|
238
|
+
}
|
|
225
239
|
if (updateRequired) {
|
|
226
240
|
lines.push(`This buildit-mcp (${SERVER_VERSION}) is older than the API supports (${minVersion} or newer). Ask the person to update it.`);
|
|
227
241
|
}
|
package/dist/toolsets/catalog.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { PROMPTS } from '../prompts.js';
|
|
7
7
|
import { RESOURCES } from '../resources.js';
|
|
8
|
-
import { listChannelsTool, readChannelTool, readThreadTool } from '../tools/chat.js';
|
|
8
|
+
import { listChannelsTool, listEmojiTool, listPinsTool, readChannelTool, readThreadTool, } from '../tools/chat.js';
|
|
9
9
|
import { addCommentTool, listCommentsTool } from '../tools/comments.js';
|
|
10
10
|
import { getItemTool, searchItemsTool } from '../tools/items-read.js';
|
|
11
11
|
import { assignItemTool, createItemTool, linkItemsTool, rankItemTool, transitionItemTool, unlinkItemsTool, updateItemTool, } from '../tools/items-write.js';
|
|
@@ -49,6 +49,8 @@ export const CATALOG = [
|
|
|
49
49
|
listChannelsTool,
|
|
50
50
|
readChannelTool,
|
|
51
51
|
readThreadTool,
|
|
52
|
+
listPinsTool,
|
|
53
|
+
listEmojiTool,
|
|
52
54
|
// admin
|
|
53
55
|
createChannelTool,
|
|
54
56
|
createProjectTool,
|
|
@@ -13,6 +13,8 @@ export function createToolContext(init) {
|
|
|
13
13
|
...(init.signal ? { signal: init.signal } : {}),
|
|
14
14
|
apiOptions,
|
|
15
15
|
call: (id, args = {}) => callOperation(init.api, id, args, apiOptions),
|
|
16
|
+
...(init.channel ? { channel: init.channel } : {}),
|
|
17
|
+
...(init.progress ? { progress: init.progress } : {}),
|
|
16
18
|
};
|
|
17
19
|
}
|
|
18
20
|
/** Identity helper that keeps the argument and output types tied to the schemas. */
|
package/dist/transports/stdio.js
CHANGED
|
@@ -1,10 +1,28 @@
|
|
|
1
|
-
import { isJSONRPCRequest, } from '@modelcontextprotocol/server';
|
|
1
|
+
import { isJSONRPCNotification, isJSONRPCRequest, PROTOCOL_VERSION_META_KEY, ProtocolErrorCode, SUPPORTED_PROTOCOL_VERSIONS, } from '@modelcontextprotocol/server';
|
|
2
2
|
import { serveStdio, StdioServerTransport, } from '@modelcontextprotocol/server/stdio';
|
|
3
3
|
import { ApiClient } from '../api/client.js';
|
|
4
|
+
import { CHANNEL_NOTIFICATION, ChannelLoop, ChannelState, } from '../channel/loop.js';
|
|
4
5
|
import { invalidatesIdentity } from '../identity.js';
|
|
5
6
|
import { createSwitchableMcpServer, DEGRADED_LIST_TTL_MS, listedByPolicy, resolveListedTools, identityFromApi, } from '../server.js';
|
|
7
|
+
import { PREEMPT_WAIT_MS } from '../channel/lock.js';
|
|
8
|
+
import { TypingIndicator } from '../channel/typing.js';
|
|
9
|
+
import { ToolApprovals } from '../channel/approvals.js';
|
|
10
|
+
import { APPROVAL_HOOK_TOOLS, createApprovalHookTools, READ_ONLY_SAFE_HOOKS, createApproveTool, } from '../tools/approve.js';
|
|
11
|
+
import { createAskTool } from '../tools/ask.js';
|
|
12
|
+
import { createReplyTool, createTurnEndedTool } from '../tools/reply.js';
|
|
13
|
+
import { createListThreadsTool } from '../tools/threads.js';
|
|
6
14
|
import { CATALOG } from '../toolsets/catalog.js';
|
|
7
15
|
import { compareVersions, SERVER_VERSION } from '../version.js';
|
|
16
|
+
/**
|
|
17
|
+
* The plugin's AskUserQuestion and PermissionRequest hooks' timeout, in
|
|
18
|
+
* seconds (plugin/hooks/hooks.json). A card's wait ends a minute before it,
|
|
19
|
+
* so the answer arrives in time.
|
|
20
|
+
*/
|
|
21
|
+
export const HOOK_TIMEOUT_S = 3600;
|
|
22
|
+
/** How long a hook's card waits: the configured minutes, a minute under the hook's timeout at most. */
|
|
23
|
+
export function hookWaitSeconds(minutes) {
|
|
24
|
+
return Math.min(minutes * 60, HOOK_TIMEOUT_S - 60);
|
|
25
|
+
}
|
|
8
26
|
/** A known identity is read again at most this often. */
|
|
9
27
|
export const IDENTITY_REFRESH_MS = 60_000;
|
|
10
28
|
/** Methods whose answer depends on the token's scopes: they wait for a due refresh. */
|
|
@@ -111,13 +129,17 @@ export class StdioIdentity {
|
|
|
111
129
|
class GatedTransport {
|
|
112
130
|
inner;
|
|
113
131
|
before;
|
|
132
|
+
closed;
|
|
114
133
|
onclose;
|
|
115
134
|
onerror;
|
|
116
135
|
onmessage;
|
|
117
136
|
queue = Promise.resolve();
|
|
118
|
-
constructor(inner, before
|
|
137
|
+
constructor(inner, before,
|
|
138
|
+
/** Told when the connection closes (stdin ended, or close()). */
|
|
139
|
+
closed = () => undefined) {
|
|
119
140
|
this.inner = inner;
|
|
120
141
|
this.before = before;
|
|
142
|
+
this.closed = closed;
|
|
121
143
|
}
|
|
122
144
|
start() {
|
|
123
145
|
this.inner.onmessage = (message, extra) => {
|
|
@@ -129,6 +151,71 @@ class GatedTransport {
|
|
|
129
151
|
});
|
|
130
152
|
};
|
|
131
153
|
this.inner.onerror = (error) => this.onerror?.(error);
|
|
154
|
+
this.inner.onclose = () => {
|
|
155
|
+
this.closed();
|
|
156
|
+
this.onclose?.();
|
|
157
|
+
};
|
|
158
|
+
return this.inner.start();
|
|
159
|
+
}
|
|
160
|
+
send(message, options) {
|
|
161
|
+
return this.inner.send(message, options);
|
|
162
|
+
}
|
|
163
|
+
close() {
|
|
164
|
+
return this.inner.close();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Whether a message claims a protocol revision in its _meta (the 2026-07-28 way of opening). */
|
|
168
|
+
function claimsRevision(message) {
|
|
169
|
+
if (!isJSONRPCRequest(message) && !isJSONRPCNotification(message))
|
|
170
|
+
return false;
|
|
171
|
+
const params = message.params;
|
|
172
|
+
return params?._meta?.[PROTOCOL_VERSION_META_KEY] !== undefined;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Keeps a connection on the 2025 handshake (channel mode): until
|
|
176
|
+
* `initialize`, a request that claims a revision is answered with the
|
|
177
|
+
* unsupported-version error naming only the 2025-era revisions, which makes
|
|
178
|
+
* a client fall back to `initialize`; such a notification is dropped.
|
|
179
|
+
*/
|
|
180
|
+
export class LegacyOnlyTransport {
|
|
181
|
+
inner;
|
|
182
|
+
onclose;
|
|
183
|
+
onerror;
|
|
184
|
+
onmessage;
|
|
185
|
+
initialized = false;
|
|
186
|
+
constructor(inner) {
|
|
187
|
+
this.inner = inner;
|
|
188
|
+
}
|
|
189
|
+
start() {
|
|
190
|
+
this.inner.onmessage = (message, extra) => {
|
|
191
|
+
if (!this.initialized && claimsRevision(message)) {
|
|
192
|
+
if (isJSONRPCRequest(message)) {
|
|
193
|
+
const meta = message.params._meta ?? {};
|
|
194
|
+
const requested = meta[PROTOCOL_VERSION_META_KEY];
|
|
195
|
+
void this.inner
|
|
196
|
+
.send({
|
|
197
|
+
jsonrpc: '2.0',
|
|
198
|
+
id: message.id,
|
|
199
|
+
error: {
|
|
200
|
+
code: ProtocolErrorCode.UnsupportedProtocolVersion,
|
|
201
|
+
message: 'Unsupported protocol version: this channel server speaks the 2025 handshake.',
|
|
202
|
+
data: {
|
|
203
|
+
supported: [...SUPPORTED_PROTOCOL_VERSIONS],
|
|
204
|
+
requested: typeof requested === 'string' ? requested : 'unknown',
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
})
|
|
208
|
+
.catch((error) => {
|
|
209
|
+
this.onerror?.(error instanceof Error ? error : new Error(String(error)));
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (isJSONRPCRequest(message) && message.method === 'initialize')
|
|
215
|
+
this.initialized = true;
|
|
216
|
+
this.onmessage?.(message, extra);
|
|
217
|
+
};
|
|
218
|
+
this.inner.onerror = (error) => this.onerror?.(error);
|
|
132
219
|
this.inner.onclose = () => this.onclose?.();
|
|
133
220
|
return this.inner.start();
|
|
134
221
|
}
|
|
@@ -139,6 +226,56 @@ class GatedTransport {
|
|
|
139
226
|
return this.inner.close();
|
|
140
227
|
}
|
|
141
228
|
}
|
|
229
|
+
/** The tools that post in a thread and stop its "is writing" themselves. */
|
|
230
|
+
const POSTING_TOOLS = new Set(['reply', 'ask', 'turn_ended', 'list_threads']);
|
|
231
|
+
/**
|
|
232
|
+
* A tool call is activity for "is writing": for the thread it names (its
|
|
233
|
+
* thread_id or card_thread, or an item's key that the session works on),
|
|
234
|
+
* else for the only working thread. A call that works on an active item's
|
|
235
|
+
* thread (a worker reading or changing its item) shows "is writing" there
|
|
236
|
+
* again; the posting tools stop it themselves.
|
|
237
|
+
*/
|
|
238
|
+
function noteActivity(state, tool, args) {
|
|
239
|
+
const typing = state.typing;
|
|
240
|
+
if (typing === undefined)
|
|
241
|
+
return;
|
|
242
|
+
// The plugin's approval hooks: tool_seen counts the activity itself.
|
|
243
|
+
if (APPROVAL_HOOK_TOOLS.has(tool))
|
|
244
|
+
return;
|
|
245
|
+
const a = (args !== null && typeof args === 'object' ? args : {});
|
|
246
|
+
const str = (v) => typeof v === 'string' && v !== '' ? v : undefined;
|
|
247
|
+
const ref = str(a.thread_id) ?? str(a.card_thread) ?? str(a.item);
|
|
248
|
+
const key = ref?.startsWith('#') ? ref.slice(1) : ref;
|
|
249
|
+
const thread = key === undefined
|
|
250
|
+
? undefined
|
|
251
|
+
: state.activeItems.has(key)
|
|
252
|
+
? key
|
|
253
|
+
: state.items.get(key.toUpperCase());
|
|
254
|
+
if (thread === undefined || !state.activeItems.has(thread)) {
|
|
255
|
+
typing.activity(key !== undefined && thread === undefined ? key : undefined);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (POSTING_TOOLS.has(tool))
|
|
259
|
+
typing.activity(thread);
|
|
260
|
+
else
|
|
261
|
+
typing.work(thread);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The tools only channel mode has (reply, ask, list_threads, and the
|
|
265
|
+
* plugin's hooks: turn_ended, approve and the hooks that end approvals),
|
|
266
|
+
* under the operator's read-only and exclude rules.
|
|
267
|
+
*/
|
|
268
|
+
function channelTools(policy, state, ask, approve) {
|
|
269
|
+
return [
|
|
270
|
+
createReplyTool(state),
|
|
271
|
+
createAskTool(state, ask),
|
|
272
|
+
createListThreadsTool(state),
|
|
273
|
+
createTurnEndedTool(state),
|
|
274
|
+
createApproveTool(state, approve),
|
|
275
|
+
...createApprovalHookTools(state),
|
|
276
|
+
].filter((tool) => (!policy.readOnly || tool.annotations.readOnlyHint || READ_ONLY_SAFE_HOOKS.has(tool.name)) &&
|
|
277
|
+
!policy.excludeTools.includes(tool.name));
|
|
278
|
+
}
|
|
142
279
|
export function startStdio(config, logger, options = {}) {
|
|
143
280
|
if (config.token === undefined)
|
|
144
281
|
throw new Error('stdio mode needs a token');
|
|
@@ -159,7 +296,56 @@ export function startStdio(config, logger, options = {}) {
|
|
|
159
296
|
invalidations.listener();
|
|
160
297
|
},
|
|
161
298
|
});
|
|
162
|
-
const
|
|
299
|
+
const channel = config.channel !== undefined;
|
|
300
|
+
const state = new ChannelState();
|
|
301
|
+
state.isChannel = options.channelOwner ?? true;
|
|
302
|
+
state.folder = config.channel?.folder;
|
|
303
|
+
if (channel && state.isChannel) {
|
|
304
|
+
state.typing = new TypingIndicator({
|
|
305
|
+
api,
|
|
306
|
+
logger: logger.child({ component: 'typing' }),
|
|
307
|
+
...options.typing,
|
|
308
|
+
});
|
|
309
|
+
state.approvals = new ToolApprovals({
|
|
310
|
+
logger: logger.child({ component: 'approvals' }),
|
|
311
|
+
journal: options.approvalJournal,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const extraTools = channel
|
|
315
|
+
? channelTools(policy, state, {
|
|
316
|
+
questionsInApp: config.ask.questionsInApp,
|
|
317
|
+
hookWaitS: hookWaitSeconds(config.ask.timeoutMinutes),
|
|
318
|
+
...options.ask,
|
|
319
|
+
}, {
|
|
320
|
+
approvalsInApp: config.approvals.inApp,
|
|
321
|
+
hookWaitS: hookWaitSeconds(config.approvals.timeoutMinutes),
|
|
322
|
+
...options.approve,
|
|
323
|
+
})
|
|
324
|
+
: [];
|
|
325
|
+
// Once a channel's session has ended, the listing stays as it last was
|
|
326
|
+
// (without reply), so Claude keeps its tools, which then say why.
|
|
327
|
+
let lastKnown;
|
|
328
|
+
const isEnded = () => state.ended !== undefined;
|
|
329
|
+
const frozen = () => {
|
|
330
|
+
const base = lastKnown ?? {
|
|
331
|
+
...listedByPolicy(catalog, policy),
|
|
332
|
+
identity: 'unverified',
|
|
333
|
+
};
|
|
334
|
+
return {
|
|
335
|
+
...base,
|
|
336
|
+
tools: base.tools.filter((t) => !extraTools.includes(t) || t.runsAfterSessionEnd === true),
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
const identity = new StdioIdentity(async () => {
|
|
340
|
+
if (isEnded())
|
|
341
|
+
return frozen();
|
|
342
|
+
const listed = await resolveListedTools(identityFromApi(api), catalog, policy, logger);
|
|
343
|
+
const shown = extraTools.length > 0 ? { ...listed, tools: [...listed.tools, ...extraTools] } : listed;
|
|
344
|
+
if (shown.identity === 'known')
|
|
345
|
+
lastKnown = shown;
|
|
346
|
+
// The session may have ended while the identity was read.
|
|
347
|
+
return isEnded() ? frozen() : shown;
|
|
348
|
+
}, logger, options.now);
|
|
163
349
|
invalidations.listener = () => {
|
|
164
350
|
identity.invalidate();
|
|
165
351
|
};
|
|
@@ -182,22 +368,90 @@ export function startStdio(config, logger, options = {}) {
|
|
|
182
368
|
const wire = options.stdin && options.stdout
|
|
183
369
|
? new StdioServerTransport(options.stdin, options.stdout)
|
|
184
370
|
: new StdioServerTransport();
|
|
185
|
-
|
|
371
|
+
let loop;
|
|
372
|
+
const stopLoop = () => loop?.stop() ?? Promise.resolve();
|
|
373
|
+
const handle = serveStdio(async () => {
|
|
186
374
|
const listed = await identity.refresh();
|
|
187
375
|
const switchable = createSwitchableMcpServer({
|
|
188
|
-
tools: allowed.tools,
|
|
376
|
+
tools: [...allowed.tools, ...extraTools],
|
|
189
377
|
resources: allowed.resources,
|
|
190
378
|
prompts: allowed.prompts,
|
|
191
379
|
api,
|
|
192
380
|
logger,
|
|
381
|
+
channel,
|
|
382
|
+
...(channel
|
|
383
|
+
? {
|
|
384
|
+
channelState: state,
|
|
385
|
+
...(config.channel?.fromPlugin === true
|
|
386
|
+
? { lead: { maxParallelItems: config.ask.maxParallelItems } }
|
|
387
|
+
: {}),
|
|
388
|
+
onToolCall: (tool, args) => {
|
|
389
|
+
noteActivity(state, tool, args);
|
|
390
|
+
},
|
|
391
|
+
sessionEnded: () => state.ended,
|
|
392
|
+
onSessionOver: (code) => {
|
|
393
|
+
void loop?.end(code);
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
: {}),
|
|
193
397
|
...(listed.identity === 'known' ? {} : { toolsListTtlMs: DEGRADED_LIST_TTL_MS }),
|
|
194
398
|
});
|
|
195
399
|
identity.attach(switchable);
|
|
400
|
+
if (channel) {
|
|
401
|
+
const mcp = switchable.server.server;
|
|
402
|
+
// Notifications sent before the client has initialized would be lost.
|
|
403
|
+
mcp.oninitialized = () => {
|
|
404
|
+
if (loop !== undefined)
|
|
405
|
+
return;
|
|
406
|
+
if (!state.isChannel) {
|
|
407
|
+
logger.warn('another buildit server in this folder receives the messages from buildIt.Social (it holds the channel lock); this one serves its tools without the channel. Remove the extra server (claude mcp remove <name>; "buildit-mcp status" names them)');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
loop = new ChannelLoop({
|
|
411
|
+
api,
|
|
412
|
+
logger: logger.child({ component: 'channel' }),
|
|
413
|
+
state,
|
|
414
|
+
notify: (params) => mcp.notification({ method: CHANNEL_NOTIFICATION, params }),
|
|
415
|
+
...(options.channelLock === undefined
|
|
416
|
+
? {}
|
|
417
|
+
: {
|
|
418
|
+
owns: () => options.channelLock?.owns() ?? true,
|
|
419
|
+
...(options.channelLock.preempted === undefined
|
|
420
|
+
? {}
|
|
421
|
+
: { firstPollDelayMs: options.preemptWaitMs ?? PREEMPT_WAIT_MS }),
|
|
422
|
+
}),
|
|
423
|
+
onEnded: async (reason) => {
|
|
424
|
+
if (state.ended !== undefined) {
|
|
425
|
+
// The server stays up: Claude keeps its tools (which now say the session
|
|
426
|
+
// ended), without reply.
|
|
427
|
+
switchable.show(frozen());
|
|
428
|
+
try {
|
|
429
|
+
switchable.server.sendToolListChanged();
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// A client that is gone can't be told.
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
await options.onSessionEnded?.(reason);
|
|
436
|
+
},
|
|
437
|
+
...options.channelLoop,
|
|
438
|
+
});
|
|
439
|
+
void loop.start();
|
|
440
|
+
};
|
|
441
|
+
}
|
|
196
442
|
return switchable.server;
|
|
197
443
|
}, {
|
|
198
444
|
onerror: (error) => {
|
|
199
445
|
logger.error('stdio transport error', { error });
|
|
200
446
|
},
|
|
201
|
-
transport: new GatedTransport(wire, (message) => identity.beforeMessage(message))
|
|
447
|
+
transport: new GatedTransport(channel ? new LegacyOnlyTransport(wire) : wire, (message) => identity.beforeMessage(message), () => {
|
|
448
|
+
void stopLoop();
|
|
449
|
+
}),
|
|
202
450
|
});
|
|
451
|
+
return {
|
|
452
|
+
async close() {
|
|
453
|
+
await stopLoop();
|
|
454
|
+
await handle.close();
|
|
455
|
+
},
|
|
456
|
+
};
|
|
203
457
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kammcs/buildit-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.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",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"inspect:docker": "npm run build && docker build -t buildit-mcp:inspect . && tsx scripts/inspect.ts --image buildit-mcp:inspect",
|
|
47
47
|
"image:publish": "tsx scripts/publish-image.ts",
|
|
48
48
|
"bundle": "npm run build && tsx scripts/bundle.ts",
|
|
49
|
+
"dev-marketplace": "tsx scripts/dev-marketplace.ts",
|
|
49
50
|
"conformance": "npm run build && tsx scripts/conformance.ts",
|
|
50
51
|
"generate": "tsx scripts/generate.ts",
|
|
51
52
|
"privacy": "tsx scripts/privacy.ts",
|
|
@@ -73,5 +74,8 @@
|
|
|
73
74
|
"typescript": "6.0.3",
|
|
74
75
|
"typescript-eslint": "8.71.1",
|
|
75
76
|
"vitest": "5.0.3"
|
|
77
|
+
},
|
|
78
|
+
"optionalDependencies": {
|
|
79
|
+
"@napi-rs/keyring": "2.1.1"
|
|
76
80
|
}
|
|
77
81
|
}
|