@kammcs/buildit-mcp 0.0.0-stage → 0.2.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.
@@ -0,0 +1,466 @@
1
+ /**
2
+ * Marking text that people wrote.
3
+ *
4
+ * Titles, descriptions, comments, pages, chat messages and names are written
5
+ * by people (guests among them) and read by an agent, so they can carry text
6
+ * aimed at the agent ("ignore previous instructions and ..."). Every tool that
7
+ * returns such text wraps it:
8
+ *
9
+ * <untrusted_content id="3f9c0a7d21" source="comment" author="Ana">
10
+ * ...
11
+ * </untrusted_content id="3f9c0a7d21">
12
+ *
13
+ * and the server's instructions tell the agent that anything inside is data,
14
+ * never instructions, and that a block ends only at a closing tag with its
15
+ * id. The id is random for each tool result (or resource read), so text
16
+ * written before the call can't know it. This is a best-effort signal, not a
17
+ * security boundary; the boundaries are the token's scopes and limits, the
18
+ * API's permission checks, and preview-then-confirm for destructive changes.
19
+ *
20
+ * So that the content can't seem to end the block early, or fake a new one,
21
+ * the block's name is also defused wherever it could form a tag inside,
22
+ * look-alikes included; text that only mentions the name is left exactly as
23
+ * written (see neutralize).
24
+ */
25
+ import { AsyncLocalStorage } from 'node:async_hooks';
26
+ import { randomBytes } from 'node:crypto';
27
+ export const UNTRUSTED_TAG = 'untrusted_content';
28
+ export const DEFAULT_MAX_CHARS = 8000;
29
+ const MAX_ATTRIBUTE_CHARS = 120;
30
+ const DEFAULT_MORE_HINT = 'Ask for the full text (for example with detail="full") to read the rest.';
31
+ const ATTRIBUTE_NAME = /^[a-z][a-z_]{0,30}$/;
32
+ // ---------------------------------------------------------------------------
33
+ // Block ids
34
+ // ---------------------------------------------------------------------------
35
+ const blockIds = new AsyncLocalStorage();
36
+ /** A fresh random block id: 10 hex characters. */
37
+ export function newBlockId() {
38
+ return randomBytes(5).toString('hex');
39
+ }
40
+ /**
41
+ * Runs `fn` with one block id for every block it wraps, so one tool result
42
+ * (or resource read) uses one id. Outside such a run, each block gets its
43
+ * own random id.
44
+ */
45
+ export function withBlockId(fn, id = newBlockId()) {
46
+ return blockIds.run(id, fn);
47
+ }
48
+ function currentBlockId() {
49
+ return blockIds.getStore() ?? newBlockId();
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // Defusing the block's name
53
+ // ---------------------------------------------------------------------------
54
+ /**
55
+ * Cyrillic, Greek and other letters that look like the Latin letters of
56
+ * "untrusted" and "content" (lower case; text is lower-cased first).
57
+ */
58
+ const LOOK_ALIKES = {
59
+ '\u{3C5}': 'u', // Greek upsilon
60
+ '\u{57D}': 'u', // Armenian seh
61
+ '\u{1D1C}': 'u', // small capital U
62
+ '\u{3BD}': 'n', // Greek nu (from upper-case N)
63
+ '\u{3B7}': 'n', // Greek eta
64
+ '\u{43F}': 'n', // Cyrillic pe
65
+ '\u{274}': 'n', // small capital N
66
+ '\u{3C4}': 't', // Greek tau
67
+ '\u{442}': 't', // Cyrillic te
68
+ '\u{1D1B}': 't', // small capital T
69
+ '\u{433}': 'r', // Cyrillic ghe
70
+ '\u{280}': 'r', // small capital R
71
+ '\u{455}': 's', // Cyrillic dze
72
+ '\u{A731}': 's', // small capital S
73
+ '\u{435}': 'e', // Cyrillic ie
74
+ '\u{3B5}': 'e', // Greek epsilon
75
+ '\u{1D07}': 'e', // small capital E
76
+ '\u{501}': 'd', // Cyrillic komi de
77
+ '\u{1D05}': 'd', // small capital D
78
+ '\u{441}': 'c', // Cyrillic es
79
+ '\u{3F2}': 'c', // Greek lunate sigma
80
+ '\u{1D04}': 'c', // small capital C
81
+ '\u{43E}': 'o', // Cyrillic o
82
+ '\u{3BF}': 'o', // Greek omicron
83
+ '\u{3C3}': 'o', // Greek sigma
84
+ '\u{1D0F}': 'o', // small capital O
85
+ };
86
+ /**
87
+ * Characters that render as nothing or only change the one before them,
88
+ * dropped from the shadow copy: default ignorable code points (invisible
89
+ * formatting characters, tag characters U+E0000 to U+E007F, and the
90
+ * variation selectors, including the supplementary U+E0100 to U+E01EF and
91
+ * the Mongolian U+180B to U+180F) and combining marks.
92
+ */
93
+ const STRIPPED = /[\p{Default_Ignorable_Code_Point}\p{M}]/u;
94
+ /** The name in the shadow copy: the two words with any punctuation, spaces or controls between. */
95
+ const NAME_IN_SHADOW = /untrusted[\p{P}\p{Z}\p{Cc}\s]*content/gu;
96
+ /** What may sit between a bracket and the name: spaces, controls and the stripped characters. */
97
+ const GAP = /[\s\p{Cc}\p{Default_Ignorable_Code_Point}\p{M}]/u;
98
+ /** Slashes and their look-alikes. */
99
+ const SLASH = /[/\\\u{2044}\u{2215}\u{29F8}\u{2571}\u{FF0F}\u{FF3C}\u{27CB}]/u;
100
+ /**
101
+ * Opening angle brackets and their look-alikes. Compatibility forms (<, ﹤,
102
+ * 〈, ...) are found through their shadow (NFKD, marks dropped).
103
+ */
104
+ const OPENERS = new Set([
105
+ '<',
106
+ '\u{2C2}', // ˂ modifier letter left arrowhead
107
+ '\u{2039}', // ‹ single left-pointing angle quotation mark
108
+ '\u{3008}', // 〈 CJK left angle bracket (and U+2329 through NFKD)
109
+ '\u{27E8}', // ⟨ mathematical left angle bracket
110
+ '\u{27EA}', // ⟪ mathematical left double angle bracket
111
+ '\u{276C}', // ❬ medium left-pointing angle bracket ornament
112
+ '\u{276E}', // ❮ heavy left-pointing angle quotation mark ornament
113
+ '\u{2770}', // ❰ heavy left-pointing angle bracket ornament
114
+ '\u{29FC}', // ⧼ left-pointing curved angle bracket
115
+ '\u{2991}', // ⦑ left angle bracket with dot
116
+ '\u{2993}', // ⦓ left arc less-than bracket
117
+ '\u{227A}', // ≺ precedes
118
+ '\u{1438}', // ᐸ Canadian syllabics pa
119
+ ]);
120
+ /** Closing angle brackets and their look-alikes, as OPENERS. */
121
+ const CLOSERS = new Set([
122
+ '>',
123
+ '\u{2C3}', // ˃
124
+ '\u{203A}', // ›
125
+ '\u{3009}', // 〉 (and U+232A through NFKD)
126
+ '\u{27E9}', // ⟩
127
+ '\u{27EB}', // ⟫
128
+ '\u{276D}', // ❭
129
+ '\u{276F}', // ❯
130
+ '\u{2771}', // ❱
131
+ '\u{29FD}', // ⧽
132
+ '\u{2992}', // ⦒
133
+ '\u{2994}', // ⦔
134
+ '\u{227B}', // ≻ succeeds
135
+ '\u{1433}', // ᐳ Canadian syllabics po
136
+ ]);
137
+ /** `&lt;` as an HTML entity, ending at the end of the string. */
138
+ const OPEN_ENTITY = /&(?:lt|#0*60|#x0*3c);$/i;
139
+ /** `&gt;` as an HTML entity, at the start of the string. */
140
+ const CLOSE_ENTITY = /^&(?:gt|#0*62|#x0*3e);/i;
141
+ /** How far back or ahead an entity can reach, in UTF-16 units. */
142
+ const ENTITY_REACH = 16;
143
+ /** What the name can be glued to as the end of a word, path, URL or anchor (not a tag). */
144
+ const WORDLIKE = /[\p{L}\p{N}#/.\-_~%+=?&@:]/u;
145
+ /** Characters an attribute name or an unquoted value can't contain (besides gaps and brackets). */
146
+ const NOT_IN_ATTRIBUTE = /["'`=<>]/;
147
+ // eslint-disable-next-line no-control-regex -- ASCII is the fast path
148
+ const ASCII_ONLY = /^[\u0000-\u007F]*$/;
149
+ /** What one code point contributes to the shadow copy. */
150
+ function shadowOf(ch, cache) {
151
+ const cached = cache.get(ch);
152
+ if (cached !== undefined)
153
+ return cached;
154
+ let out = '';
155
+ if (!STRIPPED.test(ch)) {
156
+ for (const c of ch.normalize('NFKD').toLowerCase()) {
157
+ if (STRIPPED.test(c))
158
+ continue;
159
+ out += LOOK_ALIKES[c] ?? c;
160
+ }
161
+ }
162
+ cache.set(ch, out);
163
+ return out;
164
+ }
165
+ /** Where the name appears in `text`, as [start, end) spans of `text`, in order. */
166
+ function nameSpans(text, cache) {
167
+ const spans = [];
168
+ if (ASCII_ONLY.test(text)) {
169
+ // One shadow character per character: the indexes are the same.
170
+ for (const m of text.toLowerCase().matchAll(NAME_IN_SHADOW)) {
171
+ spans.push([m.index, m.index + m[0].length]);
172
+ }
173
+ return spans;
174
+ }
175
+ // The shadow copy: NFKD, lower case, without invisible characters and
176
+ // marks, look-alikes folded to Latin. Each of its UTF-16 units remembers
177
+ // the code point of `text` it came from (start and length).
178
+ let shadow = '';
179
+ const from = [];
180
+ const size = [];
181
+ for (let i = 0; i < text.length;) {
182
+ const cp = text.codePointAt(i) ?? 0;
183
+ const len = cp > 0xffff ? 2 : 1;
184
+ const piece = cp < 0x80 ? text.charAt(i).toLowerCase() : shadowOf(text.slice(i, i + len), cache);
185
+ shadow += piece;
186
+ for (let k = 0; k < piece.length; k++) {
187
+ from.push(i);
188
+ size.push(len);
189
+ }
190
+ i += len;
191
+ }
192
+ for (const m of shadow.matchAll(NAME_IN_SHADOW)) {
193
+ const last = m.index + m[0].length - 1;
194
+ spans.push([from[m.index] ?? 0, (from[last] ?? 0) + (size[last] ?? 1)]);
195
+ }
196
+ return spans;
197
+ }
198
+ /** The code point that ends just before `index`, and its length in UTF-16 units. */
199
+ function before(text, index) {
200
+ if (index <= 0)
201
+ return undefined;
202
+ const low = text.charCodeAt(index - 1);
203
+ if (index >= 2 && low >= 0xdc00 && low <= 0xdfff) {
204
+ const high = text.charCodeAt(index - 2);
205
+ if (high >= 0xd800 && high <= 0xdbff)
206
+ return { ch: text.slice(index - 2, index), len: 2 };
207
+ }
208
+ return { ch: text.charAt(index - 1), len: 1 };
209
+ }
210
+ /** The code point that starts at `index`. */
211
+ function at(text, index) {
212
+ const cp = text.codePointAt(index);
213
+ return cp === undefined ? '' : String.fromCodePoint(cp);
214
+ }
215
+ function isBracket(ch, set, cache) {
216
+ return set.has(ch) || (ch.charCodeAt(0) > 0x7f && set.has(shadowOf(ch, cache)));
217
+ }
218
+ /** Skips gap characters from `index`; the index after them. */
219
+ function skipGap(text, index) {
220
+ let i = index;
221
+ while (i < text.length) {
222
+ const ch = at(text, i);
223
+ if (!GAP.test(ch))
224
+ break;
225
+ i += ch.length;
226
+ }
227
+ return i;
228
+ }
229
+ /** Skips a run of characters that may make up an attribute name or an unquoted value. */
230
+ function skipAttributeRun(text, index, cache, slashes) {
231
+ let i = index;
232
+ while (i < text.length) {
233
+ const ch = at(text, i);
234
+ if (GAP.test(ch) || NOT_IN_ATTRIBUTE.test(ch))
235
+ break;
236
+ if (!slashes && SLASH.test(ch))
237
+ break;
238
+ if (isBracket(ch, CLOSERS, cache) || isBracket(ch, OPENERS, cache))
239
+ break;
240
+ i += ch.length;
241
+ }
242
+ return i;
243
+ }
244
+ /**
245
+ * Whether the name that ends at `end` is followed by the rest of a tag: a
246
+ * closing bracket (or `&gt;`), right after it or after attributes
247
+ * (`key="value"`, `key='value'` or `key=value`, each after a space), with an
248
+ * optional slash before the bracket. A space and a bracket with no attribute
249
+ * between, as in "Settings > untrusted_content > Ids", is prose. `memo`
250
+ * keeps the answer for each state the scan passes (a position, and whether
251
+ * an attribute came before it), so that scans from many names share their
252
+ * work and the whole is linear.
253
+ */
254
+ function closesTag(text, end, cache, memo) {
255
+ const isCloserAt = (i) => i < text.length &&
256
+ (isBracket(at(text, i), CLOSERS, cache) || CLOSE_ENTITY.test(text.slice(i, i + ENTITY_REACH)));
257
+ const path = [];
258
+ let answer = false;
259
+ let i = end;
260
+ let attributes = false;
261
+ for (;;) {
262
+ const state = 2 * i + (attributes ? 1 : 0);
263
+ const known = memo.get(state);
264
+ if (known !== undefined) {
265
+ answer = known;
266
+ break;
267
+ }
268
+ path.push(state);
269
+ let k = skipGap(text, i);
270
+ if (k >= text.length)
271
+ break;
272
+ const spaced = /\s/.test(text.slice(i, k));
273
+ const ch = at(text, k);
274
+ if (attributes || !spaced) {
275
+ if (isCloserAt(k)) {
276
+ answer = true;
277
+ break;
278
+ }
279
+ if (SLASH.test(ch)) {
280
+ answer = isCloserAt(skipGap(text, k + ch.length));
281
+ break;
282
+ }
283
+ }
284
+ // An attribute, after at least one space.
285
+ if (k === i)
286
+ break;
287
+ const nameEnd = skipAttributeRun(text, k, cache, false);
288
+ if (nameEnd === k)
289
+ break;
290
+ k = skipGap(text, nameEnd);
291
+ if (text.charAt(k) !== '=')
292
+ break;
293
+ k = skipGap(text, k + 1);
294
+ const quote = text.charAt(k);
295
+ if (quote === '"' || quote === "'") {
296
+ const close = text.indexOf(quote, k + 1);
297
+ if (close < 0)
298
+ break;
299
+ i = close + 1;
300
+ }
301
+ else {
302
+ const valueEnd = skipAttributeRun(text, k, cache, true);
303
+ if (valueEnd === k)
304
+ break;
305
+ i = valueEnd;
306
+ }
307
+ attributes = true;
308
+ }
309
+ for (const state of path)
310
+ memo.set(state, answer);
311
+ return answer;
312
+ }
313
+ /**
314
+ * Defuses every place in `text` where the block's name could form a tag that
315
+ * opens or closes an untrusted_content block, and leaves all other text as
316
+ * it is, so that prose and Markdown that mention the name (in backticks,
317
+ * bold, headings, links, quotes, paths and so on) come back byte for byte.
318
+ *
319
+ * The name is found in any case, with any punctuation or spaces between its
320
+ * two words, in full-width, mathematical or other compatibility forms, with
321
+ * Cyrillic or Greek look-alike letters, and with invisible characters,
322
+ * combining marks or variation selectors anywhere in it: in a shadow copy of
323
+ * the text (NFKD, lower case, those characters dropped, look-alikes folded
324
+ * to Latin), mapped back.
325
+ *
326
+ * It could form a tag when, after optional spaces, invisible characters and
327
+ * one optional slash (or look-alike), an opening angle bracket stands before
328
+ * it: `<`, a look-alike (˂, 〈, ‹, ⟨, ...) or a compatibility form of one
329
+ * (<, ﹤, ...). That bracket becomes `[`, so `</untrusted_content>` reads as
330
+ * `[/untrusted_content>`: still legible, no longer a tag. When `&lt;` stands
331
+ * there instead, or the name (not glued to a word, path, URL or anchor) is
332
+ * followed by the rest of a tag (`>`, a look-alike or `&gt;`, right after it
333
+ * or after attributes; see closesTag), `[` goes just before the name. A
334
+ * name right after `[` is already
335
+ * defused and stays as it is, so running this again changes nothing.
336
+ *
337
+ * This is a second line of defence: the first is the block id, which text
338
+ * written before the call can't know. Linear in the length of the text.
339
+ */
340
+ export function neutralize(text) {
341
+ if (text.length < 'untrustedcontent'.length)
342
+ return text;
343
+ const cache = new Map();
344
+ const spans = nameSpans(text, cache);
345
+ if (spans.length === 0)
346
+ return text;
347
+ const memo = new Map();
348
+ // Edits by position: replace the character there (of this width) with "[", or insert "[".
349
+ const edits = new Map();
350
+ for (const [start, end] of spans) {
351
+ let j = start;
352
+ let slash = false;
353
+ for (;;) {
354
+ const prev = before(text, j);
355
+ if (prev === undefined)
356
+ break;
357
+ if (GAP.test(prev.ch)) {
358
+ j -= prev.len;
359
+ }
360
+ else if (!slash && SLASH.test(prev.ch)) {
361
+ slash = true;
362
+ j -= prev.len;
363
+ }
364
+ else {
365
+ break;
366
+ }
367
+ }
368
+ const opener = before(text, j);
369
+ if (opener?.ch === '[')
370
+ continue;
371
+ if (opener !== undefined && isBracket(opener.ch, OPENERS, cache)) {
372
+ edits.set(j - opener.len, opener.len);
373
+ continue;
374
+ }
375
+ if (OPEN_ENTITY.test(text.slice(Math.max(0, j - ENTITY_REACH), j))) {
376
+ edits.set(start, 0);
377
+ continue;
378
+ }
379
+ // Glued (no space) to the end of a word, path, URL or anchor: not a tag of the name.
380
+ let glued = before(text, start);
381
+ if (glued !== undefined && SLASH.test(glued.ch))
382
+ glued = before(text, start - glued.len);
383
+ if (glued !== undefined && WORDLIKE.test(glued.ch))
384
+ continue;
385
+ if (closesTag(text, end, cache, memo))
386
+ edits.set(start, 0);
387
+ }
388
+ let out = '';
389
+ let last = 0;
390
+ for (const pos of [...edits.keys()].sort((a, b) => a - b)) {
391
+ if (pos < last)
392
+ continue;
393
+ out += text.slice(last, pos) + '[';
394
+ last = pos + (edits.get(pos) ?? 0);
395
+ }
396
+ return out + text.slice(last);
397
+ }
398
+ // ---------------------------------------------------------------------------
399
+ // Wrapping
400
+ // ---------------------------------------------------------------------------
401
+ /** Cuts `text` to at most `max` characters without splitting a surrogate pair. */
402
+ function cut(text, max) {
403
+ let end = Math.max(0, max);
404
+ const code = text.charCodeAt(end - 1);
405
+ if (code >= 0xd800 && code <= 0xdbff)
406
+ end -= 1;
407
+ return text.slice(0, end);
408
+ }
409
+ function attribute(value) {
410
+ const flat = neutralize(value)
411
+ // Control characters and line breaks would let a value spill out of its attribute line.
412
+ // eslint-disable-next-line no-control-regex -- stripping control characters is the point
413
+ .replace(/[\u0000-\u001F\u007F-\u009F\u{2028}\u{2029}]+/gu, ' ')
414
+ .trim();
415
+ return cut(flat, MAX_ATTRIBUTE_CHARS)
416
+ .replace(/&/g, '&amp;')
417
+ .replace(/"/g, '&quot;')
418
+ .replace(/</g, '&lt;')
419
+ .replace(/>/g, '&gt;');
420
+ }
421
+ /** The opening tag of a block with this id and these attributes. */
422
+ function openTag(id, options) {
423
+ const attrs = [`id="${id}"`, `source="${attribute(options.source)}"`];
424
+ if (options.author !== undefined && options.author !== '') {
425
+ attrs.push(`author="${attribute(options.author)}"`);
426
+ }
427
+ for (const [key, value] of Object.entries(options.attributes ?? {})) {
428
+ if (value === undefined || value === null || value === '')
429
+ continue;
430
+ if (!ATTRIBUTE_NAME.test(key) || key === 'id' || key === 'source' || key === 'author') {
431
+ throw new Error(`Bad untrusted_content attribute name: ${key}`);
432
+ }
433
+ attrs.push(`${key}="${attribute(value)}"`);
434
+ }
435
+ return `<${UNTRUSTED_TAG} ${attrs.join(' ')}>`;
436
+ }
437
+ /**
438
+ * Wraps people-written text for an agent. Long text is cut, and a note
439
+ * outside the block (so it is the server speaking, not the author) says how
440
+ * much was cut and how to read the rest.
441
+ */
442
+ export function wrapUntrusted(text, options) {
443
+ const max = options.maxChars ?? DEFAULT_MAX_CHARS;
444
+ const truncated = text.length > max;
445
+ const body = neutralize(truncated ? cut(text, max) : text);
446
+ const id = currentBlockId();
447
+ let out = `${openTag(id, options)}\n${body}\n</${UNTRUSTED_TAG} id="${id}">`;
448
+ if (truncated) {
449
+ out += `\n[Cut at ${max} of ${text.length} characters. ${options.moreHint ?? DEFAULT_MORE_HINT}]`;
450
+ }
451
+ return out;
452
+ }
453
+ /**
454
+ * Flattens a short people-written label (a person's, project's or org's
455
+ * name) to one defused line of at most `maxChars` characters. In text
456
+ * output, labels go inside a block (wrapLines in the tools); free text
457
+ * (titles, descriptions, comments, pages, messages) uses wrapUntrusted.
458
+ */
459
+ export function sanitizeLabel(text, maxChars = 200) {
460
+ const flat = neutralize(text)
461
+ // eslint-disable-next-line no-control-regex -- stripping control characters is the point
462
+ .replace(/[\u0000-\u001F\u007F-\u009F\u{2028}\u{2029}]+/gu, ' ')
463
+ .replace(/\s+/g, ' ')
464
+ .trim();
465
+ return flat.length > maxChars ? cut(flat, maxChars - 1) + '…' : flat;
466
+ }
@@ -0,0 +1,28 @@
1
+ import { readFileSync } from 'node:fs';
2
+ /** The server's name, as reported to MCP clients and in the User-Agent header. */
3
+ export const SERVER_NAME = 'buildit-mcp';
4
+ function readVersion() {
5
+ // Both src/ (tests) and dist/ (the built server) sit one level below package.json.
6
+ const raw = readFileSync(new URL('../package.json', import.meta.url), 'utf8');
7
+ const parsed = JSON.parse(raw);
8
+ return typeof parsed.version === 'string' ? parsed.version : '0.0.0';
9
+ }
10
+ /** The server's own version, from package.json. */
11
+ export const SERVER_VERSION = readVersion();
12
+ /** Compares dotted numeric versions ("1.2.10" > "1.2.9"); pre-release tags are ignored. */
13
+ export function compareVersions(a, b) {
14
+ const parts = (v) => v
15
+ .replace(/^v/, '')
16
+ .split(/[-+]/, 1)
17
+ .join('')
18
+ .split('.')
19
+ .map((p) => Number.parseInt(p, 10) || 0);
20
+ const pa = parts(a);
21
+ const pb = parts(b);
22
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
23
+ const d = (pa[i] ?? 0) - (pb[i] ?? 0);
24
+ if (d !== 0)
25
+ return d < 0 ? -1 : 1;
26
+ }
27
+ return 0;
28
+ }
package/package.json CHANGED
@@ -1,6 +1,77 @@
1
1
  {
2
2
  "name": "@kammcs/buildit-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
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
+ "license": "MIT",
6
+ "author": "Kamm Creative Solutions",
7
+ "homepage": "https://github.com/kammcs/buildit-mcp#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/kammcs/buildit-mcp.git"
11
+ },
12
+ "bugs": "https://github.com/kammcs/buildit-mcp/issues",
13
+ "keywords": [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "buildit",
17
+ "agents"
18
+ ],
19
+ "mcpName": "io.github.kammcs/buildit-mcp",
20
+ "type": "module",
21
+ "bin": {
22
+ "buildit-mcp": "dist/index.js"
23
+ },
24
+ "main": "dist/index.js",
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE",
29
+ "SECURITY.md",
30
+ "CHANGELOG.md"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "engines": {
36
+ "node": ">=20"
37
+ },
38
+ "scripts": {
39
+ "build": "node scripts/clean.mjs && tsc -p tsconfig.build.json",
40
+ "test": "vitest run",
41
+ "lint": "tsc -p tsconfig.json --noEmit && eslint .",
42
+ "format": "prettier --write .",
43
+ "format:check": "prettier --check .",
44
+ "check": "npm run lint && npm run format:check && npm run build && npm run test && npm run privacy:pack",
45
+ "inspect": "npm run build && tsx scripts/inspect.ts",
46
+ "inspect:docker": "npm run build && docker build -t buildit-mcp:inspect . && tsx scripts/inspect.ts --image buildit-mcp:inspect",
47
+ "image:publish": "tsx scripts/publish-image.ts",
48
+ "bundle": "npm run build && tsx scripts/bundle.ts",
49
+ "conformance": "npm run build && tsx scripts/conformance.ts",
50
+ "generate": "tsx scripts/generate.ts",
51
+ "privacy": "tsx scripts/privacy.ts",
52
+ "prepublishOnly": "npm run check",
53
+ "privacy:pack": "tsx scripts/privacy.ts --pack"
54
+ },
55
+ "dependencies": {
56
+ "@hono/node-server": "2.1.3",
57
+ "@modelcontextprotocol/hono": "2.0.2",
58
+ "@modelcontextprotocol/server": "2.3.1",
59
+ "hono": "4.13.13",
60
+ "zod": "4.6.5"
61
+ },
62
+ "devDependencies": {
63
+ "@eslint/js": "10.0.1",
64
+ "@modelcontextprotocol/client": "2.3.1",
65
+ "@modelcontextprotocol/conformance": "0.2.0-alpha.12",
66
+ "@modelcontextprotocol/inspector": "2.11.0",
67
+ "@types/node": "22.20.5",
68
+ "eslint": "10.12.0",
69
+ "eslint-config-prettier": "10.1.8",
70
+ "globals": "17.13.0",
71
+ "prettier": "3.9.9",
72
+ "tsx": "4.23.15",
73
+ "typescript": "6.0.3",
74
+ "typescript-eslint": "8.71.1",
75
+ "vitest": "5.0.3"
76
+ }
77
+ }