Skip to content

maintainer-action: measure the two MCP tool pins of connector-github on a local machine and post the output (unblocks #28647) #30116

Open

Description

@objectstack-fleet

epic:#27293#1 · sunhaolin · session_017xRBcVBKAFk1ntXSjvPt7U

Filing gate: ③ a task the maintainer ordered, verbatim 「立一张维护者处理卡,让维护者在本机测试」 (2026-11-04, in this seat's session). Child of #27293 (E4). It carries the one step of #28647 that a cloud seat cannot do. ⛔ Not a claim.

Ruling-ref: 6556740185

Maintainer-action: on your own machine, run the script below with a read-only GitHub token in GITHUB_MCP_TOKEN, and post its stdout as a comment on this card — done when a comment on this card carries the script's JSON with a toolsSnapshot digest for both search_issues and issue_read

维护者速读

The script

Save as measure-github-mcp-pins.mjs, then run GITHUB_MCP_TOKEN=<token> node measure-github-mcp-pins.mjs. Its canonical JSON and digest are the ones packages/connectors/connector-mcp/src/tool-snapshot.ts defines. The seat checked the script against a local mock MCP server (SSE answers, session header, a paged tools/list, a 401 on a wrong token), and computeMcpToolsSnapshot reproduced its digests exactly.

// Measure the toolsSnapshot pins of packages/apps/connector-github (#28647).
// Node >= 18, no dependencies. Reads the token from GITHUB_MCP_TOKEN and never prints it.
import { createHash } from 'node:crypto';

const URL_ = process.env.MCP_URL ?? 'https://api.githubcopilot.com/mcp/';
const INCLUDE = ['search_issues', 'issue_read'];
const token = process.env.GITHUB_MCP_TOKEN;
if (!token) { console.error('GITHUB_MCP_TOKEN is not set'); process.exit(2); }

// The same canonical JSON and digest as packages/connectors/connector-mcp/src/tool-snapshot.ts.
const canonicalJson = (v) => {
  if (Array.isArray(v)) return `[${v.map((e) => (e === undefined ? 'null' : canonicalJson(e))).join(',')}]`;
  if (v !== null && typeof v === 'object') {
    return `{${Object.keys(v).filter((k) => v[k] !== undefined).sort()
      .map((k) => `${JSON.stringify(k)}:${canonicalJson(v[k])}`).join(',')}}`;
  }
  return JSON.stringify(v);
};
const digest = (t) => createHash('sha256')
  .update(canonicalJson({ name: t.name, description: t.description, inputSchema: t.inputSchema }), 'utf8')
  .digest('hex');

let sessionId; let nextId = 1;
async function rpc(method, params, notify = false) {
  const headers = {
    'content-type': 'application/json',
    accept: 'application/json, text/event-stream',
    authorization: `Bearer ${token}`,
    'x-mcp-readonly': 'true',
  };
  if (sessionId) headers['mcp-session-id'] = sessionId;
  const id = notify ? undefined : nextId++;
  const res = await fetch(URL_, { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '2.0', method, params, ...(notify ? {} : { id }) }) });
  sessionId = res.headers.get('mcp-session-id') ?? sessionId;
  if (notify) return undefined;
  if (!res.ok) { console.error(`${method}: HTTP ${res.status}`); process.exit(3); }
  const text = await res.text();
  const bodies = (res.headers.get('content-type') ?? '').includes('text/event-stream')
    ? text.split(/\r?\n/).filter((l) => l.startsWith('data:')).map((l) => l.slice(5).trim()).filter(Boolean)
    : [text];
  const msg = bodies.map((b) => JSON.parse(b)).find((m) => m.id === id);
  if (!msg) { console.error(`${method}: no response with id ${id}`); process.exit(3); }
  if (msg.error) { console.error(`${method}: ${JSON.stringify(msg.error)}`); process.exit(3); }
  return msg.result;
}

await rpc('initialize', { protocolVersion: '2025-03-26', capabilities: {}, clientInfo: { name: 'objectstack-pin-measure', version: '1' } });
await rpc('notifications/initialized', undefined, true);
const tools = []; let cursor;
do { const r = await rpc('tools/list', cursor ? { cursor } : {}); tools.push(...(r.tools ?? [])); cursor = r.nextCursor; } while (cursor);

const picked = INCLUDE.map((n) => tools.find((t) => t.name === n));
const missing = INCLUDE.filter((n, i) => !picked[i]);
if (missing.length) { console.error(`not listed for this token: ${missing.join(', ')} (served ${tools.length} tools) — the token is under-scoped`); process.exit(4); }

console.log(JSON.stringify({
  measuredAt: new Date().toISOString(),
  url: URL_,
  headers: { 'X-MCP-Readonly': 'true' },
  toolsSnapshot: Object.fromEntries(picked.map((t) => [t.name, digest(t)])),
  tools: picked.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
}, null, 2));

Business reason

#27293's Done-when gives each shelf listing a verifiable upstream, commit and sha256. Ruling 6556740185 (#28590 B) makes the pin set the sha256 of an MCP app. The GitHub app is the last E4 card left, and the measurement is the only part of it that needs the live server. Since 2026-11-01 the cloud environment has neither the network route nor a measurement token (#28647 comment 6557674668). Without this step, the epic cannot close.

Reader

Dedupe

Searched open and closed issues on 2026-11-04T14:55Z: toolsSnapshot 12 hits, GITHUB_MCP_TOKEN 3 hits, connector-github pins 21 hits. None asks for an off-cloud measurement. The nearest is #28647, which this card unblocks. Control connector: 768 hits. New.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions