@bridge-ds/mcp 0.8.5 → 0.10.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/dist/ops.d.ts CHANGED
@@ -71,6 +71,7 @@ export declare function searchTokens(ctx: BridgeContext, a: {
71
71
  type?: string;
72
72
  limit?: number;
73
73
  }): Promise<{
74
+ truncated?: string | undefined;
74
75
  query: string;
75
76
  mode: Combo;
76
77
  total: number;
@@ -197,6 +198,46 @@ export declare function getComponentOp(ctx: BridgeContext, a: {
197
198
  * CSS module classes, contrast notes, changelog line, existing prose kept) and a prompt for the user's own Claude
198
199
  * to write the prose that code cannot tell. Nothing is written.
199
200
  */
201
+ /**
202
+ * The tokens a component reaches: the ones its stylesheets read plus every token they alias, in any mode
203
+ * (card.bg -> color.surface.raised), so a contrast pair written on semantic tokens matches a component token.
204
+ */
205
+ export declare function reachedTokens(set: TokenSet, paths: Iterable<string>): Set<string>;
206
+ /**
207
+ * Contrast pairs that concern a component: both sides among the tokens it reaches (its text on its own surface).
208
+ * A component that sets no surface of its own (text on whatever it sits on) gets the pairs of the foregrounds it
209
+ * uses instead. Never a pair that only shares a background with it.
210
+ */
211
+ export declare function relevantPairs<P extends {
212
+ foreground: string;
213
+ background: string;
214
+ }>(pairs: P[], reached: Set<string>): P[];
215
+ /** Characters of the docs prompt per page of bridge_draft_docs: a whole prompt can pass 25,000 characters. */
216
+ export declare const DOCS_PROMPT_PAGE = 12000;
217
+ /** Splits text into pages of at most `size` characters, on line breaks when it can. */
218
+ export declare function textPages(text: string, size: number): string[];
219
+ /**
220
+ * bridge_draft_docs result for a tool call: the prompt one page at a time (page 1 by default), with a summary of
221
+ * what the draft holds and how to read the next page. The CLI prints the whole draft and does not page.
222
+ */
223
+ export declare function pagedDraftDocs<D extends {
224
+ component: string;
225
+ prompt: string;
226
+ prefill: {
227
+ tokens: string[];
228
+ contrastNotes: string[];
229
+ missing: string[];
230
+ };
231
+ }>(d: D, page?: number, size?: number): {
232
+ summary: string;
233
+ } & D & {
234
+ prompt: string;
235
+ promptPage: {
236
+ next?: string | undefined;
237
+ page: number;
238
+ pages: number;
239
+ };
240
+ };
200
241
  export declare function draftDocsOp(ctx: BridgeContext, a: {
201
242
  name: string;
202
243
  branch?: string | undefined;
@@ -737,6 +778,7 @@ export declare function getReview(ctx: BridgeContext, a: {
737
778
  export declare function reviewRoles(gh: GitHubClient, config: BridgeConfig, approvers: string[]): Promise<{
738
779
  unreadableTeams?: string[] | undefined;
739
780
  ship: string;
781
+ tier1Publish: string;
740
782
  soloOwner?: string | undefined;
741
783
  design: {
742
784
  owners: string[];
package/dist/say.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ type Surface = {
2
+ surface: 'mcp' | 'cli';
3
+ };
4
+ /** The MCP wording or the CLI wording, for this context's reader. */
5
+ export declare function say(ctx: Surface, mcp: string, cli: string): string;
6
+ /** Where a change comes from, as the labels say ("source: cli"): what the caller passed, else this context's reader. */
7
+ export declare const sourceOf: (ctx: Surface, source?: "mcp" | "cli" | undefined) => "mcp" | "cli";
8
+ /** The tool in pull request bodies and commit messages: "Bridge MCP" or "the bridge CLI". */
9
+ export declare const toolName: (source: "mcp" | "cli") => "Bridge MCP" | "the bridge CLI";
10
+ /** The same with its source label: "Bridge MCP (source: mcp)". */
11
+ export declare const toolTag: (source: "mcp" | "cli") => string;
12
+ /** How to give Bridge a token where it runs. */
13
+ export declare function addToken(ctx: Surface): string;
14
+ /** "Read-only: ... to open this as a pull request." for previews without a token. */
15
+ export declare function readOnlyPreview(ctx: Surface, what?: string): string;
16
+ /** Thrown when a rule blocks the action (the merge gate, the owner override): not a usage error. */
17
+ export declare class BlockedError extends Error {
18
+ readonly blocked = true;
19
+ constructor(message: string);
20
+ }
21
+ export declare const isBlocked: (e: unknown) => e is BlockedError;
22
+ export {};
package/dist/server.d.ts CHANGED
@@ -2,4 +2,31 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { BridgeContext } from './context.js';
3
3
  /** The package version, injected at build time; tests and source runs fall back to "dev". */
4
4
  export declare const VERSION: string;
5
- export declare function createServer(ctx: BridgeContext): McpServer;
5
+ /** The Bridge site: it serves the hosted connector (/mcp) and the official icons. */
6
+ export declare const BRIDGE_SITE = "https://bridge.floriangouloubi.com";
7
+ /**
8
+ * How clients show this server (MCP 2025-11-25, Implementation: `title` is the display name, `icons` the images; the
9
+ * SDK here supports that version). `name` stays machine-safe. Icons are absolute https URLs on the same domain as the
10
+ * hosted connector, PNG first since every client that renders icons must support it.
11
+ */
12
+ export declare const SERVER_INFO: {
13
+ name: string;
14
+ title: string;
15
+ version: string;
16
+ websiteUrl: string;
17
+ icons: {
18
+ src: string;
19
+ mimeType: string;
20
+ sizes: string[];
21
+ }[];
22
+ };
23
+ /** How a server is built for where it runs. */
24
+ export interface ServerOptions {
25
+ /**
26
+ * Tools this server neither lists nor runs. The hosted connector (Bridge inside the team's Claude) leaves out the
27
+ * merge (bridge_merge_branch and its owner override): agents never merge there, a person does. The local CLI and
28
+ * MCP server keep every tool.
29
+ */
30
+ exclude?: readonly string[] | undefined;
31
+ }
32
+ export declare function createServer(ctx: BridgeContext, opts?: ServerOptions): McpServer;
@@ -11,7 +11,7 @@ import {
11
11
  referenceImagePath,
12
12
  selectTokenFiles,
13
13
  showFile
14
- } from "./chunk-PMZGEZGX.js";
14
+ } from "./chunk-BZEPCY42.js";
15
15
 
16
16
  // ../visual/src/run.ts
17
17
  import { existsSync as existsSync3 } from "node:fs";
@@ -317,6 +317,17 @@ async function switchStory(page, storyId2) {
317
317
  );
318
318
  if (!ok) throw new Error("Storybook channel not found");
319
319
  }
320
+ async function openMeasurePage(o = {}) {
321
+ const browser = await launch(o);
322
+ try {
323
+ const ctx = await browser.newContext({ viewport: { width: 1200, height: 800 }, deviceScaleFactor: o.scale ?? 1, reducedMotion: "reduce", colorScheme: "light", locale: "en-US", serviceWorkers: "block" });
324
+ const page = await ctx.newPage();
325
+ return { page, close: () => browser.close().catch(() => void 0) };
326
+ } catch (e) {
327
+ await browser.close().catch(() => void 0);
328
+ throw e;
329
+ }
330
+ }
320
331
 
321
332
  // ../visual/src/composite.ts
322
333
  import { readFile, writeFile as writeFile2 } from "node:fs/promises";
@@ -368,6 +379,15 @@ import pixelmatch from "pixelmatch";
368
379
  import { PNG } from "pngjs";
369
380
  var decodePng = (buf) => PNG.sync.read(buf);
370
381
  var encodePng = (png) => PNG.sync.write(png, { colorType: 6 });
382
+ function cropPng(src, x, y, width, height) {
383
+ const left = Math.max(0, Math.round(x));
384
+ const top = Math.max(0, Math.round(y));
385
+ const w = Math.max(1, Math.min(src.width - left, Math.round(width)));
386
+ const h = Math.max(1, Math.min(src.height - top, Math.round(height)));
387
+ const out = new PNG({ width: w, height: h });
388
+ PNG.bitblt(src, out, left, top, w, h, 0, 0);
389
+ return out;
390
+ }
371
391
  function padTo(src, width, height) {
372
392
  if (src.width === width && src.height === height) return src;
373
393
  const out = new PNG({ width, height });
@@ -1208,9 +1228,12 @@ async function captureFigmaReferences(o) {
1208
1228
  try {
1209
1229
  const ctx = await newContext(browser, { width: 1200, height: 800 }, scale);
1210
1230
  const page = await newPage(ctx);
1211
- await page.goto(storyUrl(served.url, FIGMA_REFERENCE_STORY, brand, theme), { waitUntil: "load", timeout: 3e4 });
1212
- await page.waitForSelector("[data-fidelity-root], [data-fidelity-error]", { state: "attached", timeout: 2e4 });
1213
- await settle(page, { busy: false });
1231
+ const openStory = async () => {
1232
+ await page.goto(storyUrl(served.url, FIGMA_REFERENCE_STORY, brand, theme), { waitUntil: "load", timeout: 3e4 });
1233
+ await page.waitForSelector("[data-fidelity-root], [data-fidelity-error]", { state: "attached", timeout: 2e4 });
1234
+ await settle(page, { busy: false });
1235
+ };
1236
+ await openStory();
1214
1237
  log(`Capturing ${jobs.length} variants at ${scale}x (${brand} ${theme})`);
1215
1238
  for (const j of jobs) {
1216
1239
  const variants = manifest.components[j.component].variants;
@@ -1219,25 +1242,17 @@ async function captureFigmaReferences(o) {
1219
1242
  skipped++;
1220
1243
  continue;
1221
1244
  }
1222
- const plan = JSON.stringify(j.plan.render);
1223
- await page.evaluate(
1224
- ({ id, plan: plan2 }) => new Promise((resolve2) => {
1225
- const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
1226
- const done = () => {
1227
- clearTimeout(timer);
1228
- ch.off("storyRendered", done);
1229
- resolve2();
1230
- };
1231
- const timer = setTimeout(done, 1e4);
1232
- ch.on("storyRendered", done);
1233
- ch.emit("updateStoryArgs", { storyId: id, updatedArgs: { plan: plan2 } });
1234
- }),
1235
- { id: FIGMA_REFERENCE_STORY, plan }
1236
- );
1245
+ const shown = await renderPlan(page, JSON.stringify(j.plan.render));
1246
+ if (shown.broken) {
1247
+ variants[j.key] = { skipped: `the code example threw: ${shown.error ?? "the story did not finish rendering"}` };
1248
+ skipped++;
1249
+ await openStory();
1250
+ continue;
1251
+ }
1237
1252
  await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
1238
1253
  const error = await page.locator("[data-fidelity-error]").first().textContent({ timeout: 200 }).catch(() => null);
1239
1254
  let target = page.locator("[data-fidelity-root] > *").first();
1240
- if (!error && !await target.count()) {
1255
+ if (!error && (j.plan.render.within || j.plan.render.popup || !await target.count())) {
1241
1256
  const marked = page.locator("[data-fidelity-target]").first();
1242
1257
  await marked.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
1243
1258
  await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
@@ -1248,6 +1263,15 @@ async function captureFigmaReferences(o) {
1248
1263
  skipped++;
1249
1264
  continue;
1250
1265
  }
1266
+ const transparent = await target.evaluate((el) => {
1267
+ for (let e = el; e; e = e.parentElement) if (getComputedStyle(e).opacity === "0") return true;
1268
+ return false;
1269
+ });
1270
+ if (transparent) {
1271
+ variants[j.key] = { skipped: "the code example rendered invisible (opacity 0)" };
1272
+ skipped++;
1273
+ continue;
1274
+ }
1251
1275
  const file = referenceImagePath(j.component, j.key);
1252
1276
  const path = join4(o.out, file);
1253
1277
  await mkdir4(dirname3(path), { recursive: true });
@@ -1268,6 +1292,35 @@ async function captureFigmaReferences(o) {
1268
1292
  log(`${captured} images, ${skipped} variants skipped. ${manifestPath}`);
1269
1293
  return { manifest, manifestPath, captured, skipped };
1270
1294
  }
1295
+ async function renderPlan(page, plan) {
1296
+ const r = await page.evaluate(
1297
+ ({ id, plan: plan2 }) => new Promise((resolve2) => {
1298
+ const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
1299
+ let error;
1300
+ const failed = (p) => {
1301
+ const e = p;
1302
+ error ??= e?.message ?? e?.title ?? e?.description;
1303
+ };
1304
+ const finish = (rendered) => {
1305
+ clearTimeout(timer);
1306
+ ch.off("storyRendered", done);
1307
+ ch.off("storyThrewException", failed);
1308
+ ch.off("storyErrored", failed);
1309
+ resolve2({ rendered, ...error ? { error } : {} });
1310
+ };
1311
+ const done = () => finish(true);
1312
+ document.querySelectorAll("[data-fidelity-target]").forEach((el) => el.removeAttribute("data-fidelity-target"));
1313
+ const timer = setTimeout(() => finish(false), 1e4);
1314
+ ch.on("storyThrewException", failed);
1315
+ ch.on("storyErrored", failed);
1316
+ ch.on("storyRendered", done);
1317
+ ch.emit("updateStoryArgs", { storyId: id, updatedArgs: { plan: plan2 } });
1318
+ }),
1319
+ { id: FIGMA_REFERENCE_STORY, plan }
1320
+ );
1321
+ const errorScreen = await page.evaluate(() => document.body.classList.contains("sb-show-errordisplay"));
1322
+ return { broken: !r.rendered || !!r.error || errorScreen, ...r.error ? { error: r.error } : {} };
1323
+ }
1271
1324
  export {
1272
1325
  COMMENT_MARKER,
1273
1326
  DEFAULT_ROUTES,
@@ -1276,11 +1329,13 @@ export {
1276
1329
  boxesFromDiff,
1277
1330
  captureFigmaReferences,
1278
1331
  changedTokens,
1332
+ cropPng,
1279
1333
  decodePng,
1280
1334
  diffPngs,
1281
1335
  encodePng,
1282
1336
  hasVisualChange,
1283
1337
  linkNodeModules,
1338
+ openMeasurePage,
1284
1339
  padTo,
1285
1340
  planImpact,
1286
1341
  rankShots,
package/dist/studio.d.ts CHANGED
@@ -94,6 +94,7 @@ export declare function getSettings(ctx: BridgeContext): Promise<{
94
94
  main: string;
95
95
  roles: {
96
96
  teamsNotRead?: string[] | undefined;
97
+ tier1Publish: string;
97
98
  soloOwner?: string | undefined;
98
99
  designApproval: string[];
99
100
  techApproval: string[];
@@ -114,6 +115,16 @@ export declare function getSettings(ctx: BridgeContext): Promise<{
114
115
  visualImpact: import("@bridge-ds/core").VisualImpactMode;
115
116
  note: string;
116
117
  }>;
118
+ /**
119
+ * One count of components everywhere: the component folders (snapshot.json and llms.txt count the same), then what the
120
+ * other numbers are: layout helpers have no Figma component, and in Figma popups and items are sets of their own.
121
+ */
122
+ export declare function coverageLine(s: {
123
+ inFigma: number;
124
+ drafted: number;
125
+ layout: number;
126
+ total: number;
127
+ }, figmaSets: number): string;
117
128
  /** Which components are in Figma (Studio's "In Figma" column), and whether code images exist for the fidelity check. */
118
129
  export declare function figmaCoverage(ctx: BridgeContext, a?: {
119
130
  component?: string | undefined;
@@ -9,7 +9,9 @@ export declare const SUGGEST_RIGHTS = "Issues: read and write (Contents: read is
9
9
  */
10
10
  export declare function changesFor(ctx: BridgeContext, explicit?: ChangesMode | undefined): Promise<ChangesMode>;
11
11
  /** What to do next once a suggestion is filed. The wording matches the issue's "How to apply". */
12
- export declare const appliedNext: (n: number) => string;
12
+ export declare const appliedNext: (n: number, ctx?: {
13
+ surface: "mcp" | "cli";
14
+ }) => string;
13
15
  /** Why nothing was filed: no GitHub repo, or no token that can write issues. */
14
16
  export declare function noSuggestionAccess(ctx: BridgeContext, gh: GitHubClient | null): string;
15
17
  /**
@@ -144,6 +146,7 @@ export declare function getSuggestionOp(ctx: BridgeContext, a: {
144
146
  }[];
145
147
  } | undefined;
146
148
  approvals: {
149
+ ownerOverride?: string | undefined;
147
150
  soloOwner?: string | undefined;
148
151
  notCounted?: string[] | undefined;
149
152
  staleReviews?: string[] | undefined;
@@ -224,7 +227,13 @@ export declare function getSuggestionOp(ctx: BridgeContext, a: {
224
227
  merged: boolean;
225
228
  }[];
226
229
  }>;
227
- export type SuggestionReviewEventArg = 'approve' | 'request-changes' | 'comment' | 'decline';
230
+ export type SuggestionReviewEventArg = 'approve' | 'request-changes' | 'comment' | 'decline' | 'override';
231
+ /** The owner override on a suggestion, as Studio and History say it. */
232
+ export declare const suggestionOverrideLine: (o: {
233
+ login: string;
234
+ reason: string;
235
+ missing: readonly string[];
236
+ }) => string;
228
237
  /**
229
238
  * bridge_review_suggestion / `bridge review <n>`: approve, request changes, comment on or decline a suggestion, as the
230
239
  * token's user. Approvals count only from the owners in bridge.config.json and only on the version reviewed
@@ -242,6 +251,16 @@ export declare function reviewSuggestionOp(ctx: BridgeContext, a: {
242
251
  number: number;
243
252
  status: SuggestionStatus;
244
253
  said: string;
254
+ } | {
255
+ next: string;
256
+ warnings?: string[] | undefined;
257
+ number: number;
258
+ event: "override";
259
+ counts: boolean;
260
+ version: string;
261
+ status: SuggestionStatus;
262
+ said: string;
263
+ comment: string;
245
264
  } | {
246
265
  next: string;
247
266
  warnings?: string[] | undefined;
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@bridge-ds/mcp",
3
- "version": "0.8.5",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Bridge MCP server: lets Claude (Claude Code, Claude Desktop or any MCP client) read, check and propose changes to a DTCG design system repo through pull requests.",
6
- "license": "MIT",
6
+ "license": "FSL-1.1-MIT",
7
7
  "author": "Florian Gouloubi",
8
8
  "homepage": "https://github.com/peterbono/ai-design-system-demo/tree/main/packages/mcp#readme",
9
9
  "repository": {
@@ -51,11 +51,12 @@
51
51
  "typecheck": "tsc -p tsconfig.json --noEmit",
52
52
  "test": "npm run build && vitest run --config vitest.config.ts",
53
53
  "start": "node dist/index.js",
54
+ "readme:tools": "BRIDGE_UPDATE_README=1 vitest run --config vitest.config.ts packages/mcp/test/readme.test.ts",
54
55
  "build:package": "node ../../scripts/build-packages.mjs mcp"
55
56
  },
56
57
  "dependencies": {
57
- "@bridge-ds/core": "^0.18.0",
58
- "@bridge-ds/visual": "^0.3.12",
58
+ "@bridge-ds/core": "^0.20.0",
59
+ "@bridge-ds/visual": "^0.3.14",
59
60
  "@modelcontextprotocol/sdk": "1.31.0",
60
61
  "typescript": "5.9.3",
61
62
  "zod": "3.25.76"