From fbb7b6a265cddb0328c4c5b0ef692340b537e9e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 03:16:07 +0000 Subject: [PATCH 1/3] feat(apps): connector-github, GitHub's remote MCP server as an open-edition connector app A declarative mcp instance (http transport, X-MCP-Readonly, include allowlist of search_issues and issue_read), a bearer credentialRef, two flows using it, the listing file and an administrator README. Tested by a boot through bootStack against a fake MCP client. Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U Co-authored-by: Claude --- packages/apps/connector-github/README.md | 61 ++++++++ .../connector-github/objectstack.config.ts | 36 +++++ .../objectstack.manifest.json | 13 ++ packages/apps/connector-github/package.json | 30 ++++ .../src/connectors/github.connector.ts | 42 +++++ .../src/flows/github.flows.ts | 84 ++++++++++ .../apps/connector-github/test/boot.test.ts | 147 ++++++++++++++++++ .../connector-github/test/listing.test.ts | 64 ++++++++ packages/apps/connector-github/tsconfig.json | 15 ++ .../apps/connector-github/vitest.config.ts | 13 ++ pnpm-lock.yaml | 50 ++++++ 11 files changed, 555 insertions(+) create mode 100644 packages/apps/connector-github/README.md create mode 100644 packages/apps/connector-github/objectstack.config.ts create mode 100644 packages/apps/connector-github/objectstack.manifest.json create mode 100644 packages/apps/connector-github/package.json create mode 100644 packages/apps/connector-github/src/connectors/github.connector.ts create mode 100644 packages/apps/connector-github/src/flows/github.flows.ts create mode 100644 packages/apps/connector-github/test/boot.test.ts create mode 100644 packages/apps/connector-github/test/listing.test.ts create mode 100644 packages/apps/connector-github/tsconfig.json create mode 100644 packages/apps/connector-github/vitest.config.ts diff --git a/packages/apps/connector-github/README.md b/packages/apps/connector-github/README.md new file mode 100644 index 00000000000..8b1e08cbf70 --- /dev/null +++ b/packages/apps/connector-github/README.md @@ -0,0 +1,61 @@ +# GitHub + +Search and read GitHub issues and pull requests from your flows. The app +connects to GitHub's hosted MCP server (`https://api.githubcopilot.com/mcp/`) +in read-only mode and exposes two of its read tools as connector actions. +Nothing is written to GitHub. + +## What it adds + +- **A connector, `github_mcp`.** Its actions are `search_issues` (GitHub's + issue and pull request search syntax) and `issue_read` (one issue's details). + Any other tool the server offers stays out of your flows and agents. +- **Two flows that use it**, ready to run or to copy: + - `github_search_issues` takes a `query` (for example + `repo:acme/app is:open label:bug`) and returns the matching issues as + `search.content`. + - `github_get_issue` takes `owner`, `repo` and `issue_number` and returns the + issue as `issue.content`. + +The app adds no objects, no sample data and no demo users. An install adds the +connector and the two flows, and nothing else. + +## Credentials + +The app needs one credential. Until it is set, the `github_mcp` connector is +listed as disconnected and its flows fail, naming the credential. + +| Credential | What it is | Where to get it | Permissions it needs | +|:---|:---|:---|:---| +| `GITHUB_MCP_TOKEN` | A GitHub personal access token, sent to GitHub's MCP server as `Authorization: Bearer ` | GitHub → **Settings → Developer settings → Personal access tokens** ( for a fine-grained token) | Fine-grained token (GitHub's recommendation): access to the repositories you will search, with read-only **Issues** and **Pull requests** repository permissions; a repository owned by an organization also needs the organization to approve the token. Classic token: the `repo` scope, which both tools require | + +Set the value in **Setup → Integrations → Connector Credentials**, or in the +install dialog right after installing. An environment variable named +`GITHUB_MCP_TOKEN` on the server takes precedence over a stored value. + +Prefer a fine-grained token. With a classic token, GitHub's server hides every +tool whose scope the token lacks; if it hides `search_issues` or `issue_read`, +the connector refuses to start rather than quietly running without them, and +the reason names the missing tool. + +The token is only ever sent to GitHub. It is never stored in the app's +metadata. + +## Running it + +```bash +curl -X POST "$OS_URL/api/v1/automation/github_search_issues/trigger" \ + -H "Authorization: Bearer $SESSION_TOKEN" -H 'Content-Type: application/json' \ + -d '{"params": {"query": "repo:acme/app is:open label:bug"}}' +``` + +To use the answer elsewhere, add a `connector_action` node with +`connectorId: 'github_mcp'` to your own flow and read `.content` in a +later node. + +## Limits + +- Read only. The server is asked for read tools only (`X-MCP-Readonly: true`) + and the connector allowlists two of them. +- The token is a static personal token. Signing in with GitHub (OAuth) is not + offered. diff --git a/packages/apps/connector-github/objectstack.config.ts b/packages/apps/connector-github/objectstack.config.ts new file mode 100644 index 00000000000..e0fc5bb8212 --- /dev/null +++ b/packages/apps/connector-github/objectstack.config.ts @@ -0,0 +1,36 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { defineStack } from '@objectstack/spec'; +import { ConnectorMcpPlugin } from '@objectstack/connector-mcp'; +import { GitHubMcpConnector } from './src/connectors/github.connector.js'; +import { allFlows } from './src/flows/github.flows.js'; + +/** + * GitHub connector app — open edition (every key it uses runs on the open + * tier), so it lives in this repository and is validated by its CI against + * the runtime at HEAD. One declarative connector, two flows that use it; no + * objects, no secret. The listing is `objectstack.manifest.json`, and the + * README is what the administrator reads before installing. + */ +export default defineStack({ + manifest: { + id: 'app.objectstack.connector-github', + namespace: 'github', + version: '0.1.0', + type: 'app', + name: 'GitHub', + description: "Search and read GitHub issues from flows, through GitHub's remote MCP server.", + engines: { protocol: '^18' }, + }, + + // `automation` materializes the `connectors:` entry at boot and runs the + // flows; the executor below registers the `mcp` provider with it. + requires: ['automation'], + + // The generic `mcp` executor, zero-arg: it contributes the provider factory + // only. The transport is http, so the stdio policy does not apply. + plugins: [new ConnectorMcpPlugin()], + + connectors: [GitHubMcpConnector], + flows: allFlows, +}); diff --git a/packages/apps/connector-github/objectstack.manifest.json b/packages/apps/connector-github/objectstack.manifest.json new file mode 100644 index 00000000000..a3b7d7298d3 --- /dev/null +++ b/packages/apps/connector-github/objectstack.manifest.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://schemas.objectstack.dev/template-manifest.json", + "manifestId": "app.objectstack.connector-github", + "name": "connector-github", + "specVersion": "^18.0.0", + "displayName": "GitHub", + "description": "Search and read GitHub issues and pull requests from flows, through GitHub's remote MCP server. Read only.", + "category": "devtools", + "tags": ["github", "mcp", "connector", "issues"], + "license": "Apache-2.0", + "homepageUrl": "https://github.com/objectstack-ai/objectstack/tree/main/packages/apps/connector-github", + "readmePath": "README.md" +} diff --git a/packages/apps/connector-github/package.json b/packages/apps/connector-github/package.json new file mode 100644 index 00000000000..8eb62bb2623 --- /dev/null +++ b/packages/apps/connector-github/package.json @@ -0,0 +1,30 @@ +{ + "name": "@objectstack/connector-app-github", + "version": "0.1.0", + "description": "GitHub connector app (open edition): GitHub's remote MCP server as a declarative `mcp` connector instance with a read-tool allowlist, authenticated by a personal access token the administrator supplies through the credential store.", + "license": "Apache-2.0", + "private": true, + "main": "./objectstack.config.ts", + "types": "./objectstack.config.ts", + "exports": { + ".": "./objectstack.config.ts", + "./objectstack.config": "./objectstack.config.ts" + }, + "scripts": { + "build": "objectstack build", + "validate": "objectstack validate", + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + "@objectstack/connector-mcp": "workspace:*", + "@objectstack/spec": "workspace:*" + }, + "devDependencies": { + "@objectstack/cli": "workspace:*", + "@objectstack/verify": "workspace:*", + "@types/node": "^26.6.5", + "typescript": "^6.0.3", + "vitest": "^4.1.11" + } +} diff --git a/packages/apps/connector-github/src/connectors/github.connector.ts b/packages/apps/connector-github/src/connectors/github.connector.ts new file mode 100644 index 00000000000..300a31adee5 --- /dev/null +++ b/packages/apps/connector-github/src/connectors/github.connector.ts @@ -0,0 +1,42 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { defineConnector } from '@objectstack/spec/integration'; + +/** + * GitHub over its remote MCP server — recipe 2 of the integration recipes + * (`skills/objectstack-automation/references/integration-recipes.md`): a + * declarative `provider: 'mcp'` instance (ADR-0097) the `mcp` executor + * materializes at boot by calling the server's `tools/list`. + * + * The server URL and the token header are GitHub's own + * (github/github-mcp-server `docs/remote-server.md` and the README's remote + * configuration): `https://api.githubcopilot.com/mcp/`, authenticated by + * `Authorization: Bearer `. The `bearer` auth below is + * folded into exactly that header by the executor; the token itself is never + * in this file — `credentialRef` names it, and the administrator supplies the + * value (an environment variable of that name, or Setup → Integrations → + * Connector Credentials). + * + * `X-MCP-Readonly: true` asks the server to list only its read tools, and + * `include` narrows those to the two this app's flows call. An `include` + * entry the server does not list is refused at materialization, never + * silently dropped. + */ +export const GitHubMcpConnector = defineConnector({ + name: 'github_mcp', + label: 'GitHub (remote MCP)', + type: 'api', + description: + "GitHub's remote MCP server, read tools only: issue search and single-issue reads, authenticated by a " + + 'personal access token the administrator supplies.', + provider: 'mcp', + providerConfig: { + transport: { + kind: 'http', + url: 'https://api.githubcopilot.com/mcp/', + headers: { 'X-MCP-Readonly': 'true' }, + }, + include: ['search_issues', 'issue_read'], + }, + auth: { type: 'bearer', credentialRef: 'GITHUB_MCP_TOKEN' }, +}); diff --git a/packages/apps/connector-github/src/flows/github.flows.ts b/packages/apps/connector-github/src/flows/github.flows.ts new file mode 100644 index 00000000000..3af2acd9bad --- /dev/null +++ b/packages/apps/connector-github/src/flows/github.flows.ts @@ -0,0 +1,84 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { defineFlow } from '@objectstack/spec'; + +/** + * Search GitHub issues and pull requests. Run it with + * `POST /api/v1/automation/github_search_issues/trigger` and a body of + * `{ "params": { "query": "repo:acme/app is:open label:bug" } }`; the tool's + * answer is the run's `search.content` output. + */ +export const GitHubSearchIssuesFlow = defineFlow({ + name: 'github_search_issues', + label: 'GitHub: search issues', + description: "Searches issues and pull requests with GitHub's search syntax through the github_mcp connector.", + type: 'autolaunched', + status: 'active', + variables: [ + { name: 'query', type: 'text', isInput: true }, + { name: 'search.content', type: 'json', isOutput: true }, + ], + nodes: [ + { id: 'start', type: 'start', label: 'Start' }, + { + id: 'search', + type: 'connector_action', + label: 'search_issues', + connectorConfig: { + connectorId: 'github_mcp', + actionId: 'search_issues', + input: { query: { dialect: 'cel', source: 'query' } }, + }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'search' }, + { id: 'e2', source: 'search', target: 'end' }, + ], +}); + +/** + * Read one issue. Run it with + * `POST /api/v1/automation/github_get_issue/trigger` and a body of + * `{ "params": { "owner": "acme", "repo": "app", "issue_number": 42 } }`; the + * issue is the run's `issue.content` output. + */ +export const GitHubGetIssueFlow = defineFlow({ + name: 'github_get_issue', + label: 'GitHub: get issue', + description: 'Reads one issue (title, state, body, labels) through the github_mcp connector.', + type: 'autolaunched', + status: 'active', + variables: [ + { name: 'owner', type: 'text', isInput: true }, + { name: 'repo', type: 'text', isInput: true }, + { name: 'issue_number', type: 'number', isInput: true }, + { name: 'issue.content', type: 'json', isOutput: true }, + ], + nodes: [ + { id: 'start', type: 'start', label: 'Start' }, + { + id: 'issue', + type: 'connector_action', + label: 'issue_read', + connectorConfig: { + connectorId: 'github_mcp', + actionId: 'issue_read', + input: { + method: 'get', + owner: { dialect: 'cel', source: 'owner' }, + repo: { dialect: 'cel', source: 'repo' }, + issue_number: { dialect: 'cel', source: 'issue_number' }, + }, + }, + }, + { id: 'end', type: 'end', label: 'End' }, + ], + edges: [ + { id: 'e1', source: 'start', target: 'issue' }, + { id: 'e2', source: 'issue', target: 'end' }, + ], +}); + +export const allFlows = [GitHubSearchIssuesFlow, GitHubGetIssueFlow]; diff --git a/packages/apps/connector-github/test/boot.test.ts b/packages/apps/connector-github/test/boot.test.ts new file mode 100644 index 00000000000..de91edd5855 --- /dev/null +++ b/packages/apps/connector-github/test/boot.test.ts @@ -0,0 +1,147 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Boot test: the app, booted the way `objectstack serve` composes it + * (`bootStack` from `@objectstack/verify`), materializes its declarative + * `github_mcp` instance against a FAKE MCP client — no call reaches GitHub. + * + * The one substitution is the MCP client: the app's own `ConnectorMcpPlugin` + * is replaced (by plugin name, `BootOptions.extraPlugins`) with one whose + * `clientFactory` answers `tools/list` and `tools/call` from a fixture and + * records the transport it was handed. Everything else is the shipped app: + * the connector entry, the flows, and the administrator's path to a working + * instance — boot with no credential (the instance is degraded, naming the + * reference), set the value through the credential door, run a flow. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { fileURLToPath } from 'node:url'; +// Paid at module load, before vitest's clocked windows: both reach the whole +// runtime through their `dist/` builds. +import { bootStack, type VerifyStack } from '@objectstack/verify'; +import { ConnectorMcpPlugin, type McpClientLike, type McpTransport } from '@objectstack/connector-mcp'; +import stack from '../objectstack.config.js'; + +const APP_ROOT = fileURLToPath(new URL('..', import.meta.url)); +const REF = 'GITHUB_MCP_TOKEN'; +const FIXTURE_TOKEN = 'github_pat_fixture_not_a_real_token'; + +/** The tools the fake server lists: the two the app includes, and one it must not expose. */ +const TOOLS = [ + { name: 'search_issues', description: 'Search issues', inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } }, + { + name: 'issue_read', + description: 'Get issue details', + inputSchema: { + type: 'object', + properties: { method: { type: 'string' }, owner: { type: 'string' }, repo: { type: 'string' }, issue_number: { type: 'number' } }, + required: ['method', 'owner', 'repo', 'issue_number'], + }, + }, + { name: 'issue_write', description: 'Create or update issue', inputSchema: { type: 'object', properties: {} } }, +]; + +interface Descriptor { + name: string; + state?: string; + degradedReason?: string; + actions?: Array<{ key: string }>; +} + +describe('connector-github app — boot against a fake MCP server', () => { + const transports: McpTransport[] = []; + const calls: Array<{ name: string; args: Record }> = []; + let booted: VerifyStack; + let token: string; + const saved = process.env[REF]; + + beforeAll(async () => { + delete process.env[REF]; + const client: McpClientLike = { + listTools: async () => TOOLS, + callTool: async (name, args) => { + calls.push({ name, args }); + return { content: [{ type: 'text', text: `${name} fixture answer` }] }; + }, + close: async () => {}, + }; + booted = await bootStack(stack, { + hostRoot: APP_ROOT, + extraPlugins: [ + new ConnectorMcpPlugin({ + clientFactory: async (transport) => { + transports.push(transport); + return client; + }, + }), + ], + }); + token = await booted.signIn(); + }, 120_000); + + afterAll(async () => { + await booted?.stop(); + if (saved === undefined) delete process.env[REF]; + else process.env[REF] = saved; + }); + + const descriptor = async (): Promise => { + const res = await booted.apiAs(token, 'GET', '/automation/connectors'); + expect(res.status).toBe(200); + const body = (await res.json()) as { data?: { connectors?: Descriptor[] } }; + return body.data?.connectors?.find((c) => c.name === 'github_mcp'); + }; + + it('with no credential set, the instance is degraded naming its credentialRef and nothing was dialled', async () => { + const gh = await descriptor(); + expect(gh, 'github_mcp is not in the connector registry').toBeDefined(); + expect(gh!.state).toBe('degraded'); + expect(gh!.degradedReason).toContain(REF); + expect(transports).toEqual([]); + }); + + it('once the administrator sets the credential, it materializes with only the included read tools', async () => { + const put = await booted.apiAs(token, 'PUT', `/automation/connectors/credential-refs/${REF}`, { value: FIXTURE_TOKEN }); + expect(put.status, await put.clone().text()).toBeLessThan(300); + + let gh = await descriptor(); + for (let i = 0; i < 40 && gh?.state !== 'ready'; i++) { + await new Promise((r) => setTimeout(r, 50)); + gh = await descriptor(); + } + expect(gh?.state, gh?.degradedReason).toBe('ready'); + expect(gh!.actions?.map((a) => a.key).sort()).toEqual(['issue_read', 'search_issues']); + + // The transport the executor dialled: GitHub's URL, the read-only header, + // and the token folded into the Authorization header GitHub documents. + const http = transports.at(-1); + expect(http?.kind).toBe('http'); + if (http?.kind !== 'http') return; + expect(http.url).toBe('https://api.githubcopilot.com/mcp/'); + expect(http.headers?.['X-MCP-Readonly']).toBe('true'); + expect(http.headers?.Authorization).toBe(`Bearer ${FIXTURE_TOKEN}`); + }); + + it('github_search_issues dispatches search_issues with the query and returns its content', async () => { + const res = await booted.apiAs(token, 'POST', '/automation/github_search_issues/trigger', { + params: { query: 'repo:acme/app is:open label:bug' }, + }); + const text = await res.text(); + expect(res.status, text).toBe(200); + expect(calls.at(-1)).toEqual({ name: 'search_issues', args: { query: 'repo:acme/app is:open label:bug' } }); + expect(text).toContain('search_issues fixture answer'); + }); + + it('github_get_issue dispatches issue_read with method get and the issue coordinates', async () => { + const res = await booted.apiAs(token, 'POST', '/automation/github_get_issue/trigger', { + params: { owner: 'acme', repo: 'app', issue_number: 42 }, + }); + const text = await res.text(); + expect(res.status, text).toBe(200); + expect(calls.at(-1)).toEqual({ + name: 'issue_read', + args: { method: 'get', owner: 'acme', repo: 'app', issue_number: 42 }, + }); + expect(text).toContain('issue_read fixture answer'); + }); +}); diff --git a/packages/apps/connector-github/test/listing.test.ts b/packages/apps/connector-github/test/listing.test.ts new file mode 100644 index 00000000000..6aad53d236b --- /dev/null +++ b/packages/apps/connector-github/test/listing.test.ts @@ -0,0 +1,64 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The listing this package publishes as: `objectstack.manifest.json` parses + * against the spec's own schema for that file, names the same package id as + * the stack, points at a README that exists, and that README's credentials + * section names every credential the app's connectors reference — the + * marketplace handbook's "every credential it needs" rule, checked against + * the metadata rather than trusted. + */ + +import { describe, it, expect } from 'vitest'; +import { readFileSync, existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { TemplateManifestSchema } from '@objectstack/spec/marketplace'; +import stack from '../objectstack.config.js'; + +const manifestPath = fileURLToPath(new URL('../objectstack.manifest.json', import.meta.url)); +const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record; + +/** The README's `## Credentials` section, up to the next second-level heading. */ +function credentialsSection(readme: string): string { + const start = readme.indexOf('\n## Credentials\n'); + expect(start, 'README has no "## Credentials" section').toBeGreaterThan(-1); + const rest = readme.slice(start + 1); + const next = rest.indexOf('\n## ', 1); + return next === -1 ? rest : rest.slice(0, next); +} + +describe('connector-github listing', () => { + it('parses against TemplateManifestSchema', () => { + const parsed = TemplateManifestSchema.safeParse(manifest); + expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true); + }); + + it('names the stack manifest id as its package id', () => { + expect(manifest.manifestId).toBe(stack.manifest!.id); + }); + + it('declares no key the publisher ignores', () => { + // The handbook: the platform decides `publisher` and `isStarter`; the CLI never sends them. + expect(manifest).not.toHaveProperty('publisher'); + expect(manifest).not.toHaveProperty('isStarter'); + }); + + it("the README's credentials section names every credentialRef and where to obtain it", () => { + const readmePath = fileURLToPath(new URL(`../${String(manifest.readmePath)}`, import.meta.url)); + expect(existsSync(readmePath)).toBe(true); + const section = credentialsSection(readFileSync(readmePath, 'utf8')); + const refs = (stack.connectors ?? []) + .map((c) => (c as { auth?: { credentialRef?: string } }).auth?.credentialRef) + .filter((r): r is string => typeof r === 'string'); + expect(refs).toEqual(['GITHUB_MCP_TOKEN']); + for (const ref of refs) expect(section).toContain(`\`${ref}\``); + expect(section).toContain('https://github.com/settings/personal-access-tokens/new'); + }); + + it('carries no secret in metadata', () => { + for (const c of stack.connectors ?? []) { + expect(c).not.toHaveProperty('authentication.token'); + expect(JSON.stringify(c)).not.toMatch(/ghp_|github_pat_/); + } + }); +}); diff --git a/packages/apps/connector-github/tsconfig.json b/packages/apps/connector-github/tsconfig.json new file mode 100644 index 00000000000..3dfe4ff581b --- /dev/null +++ b/packages/apps/connector-github/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["src/**/*", "objectstack.config.ts", "test/**/*", "vitest.config.ts"] +} diff --git a/packages/apps/connector-github/vitest.config.ts b/packages/apps/connector-github/vitest.config.ts new file mode 100644 index 00000000000..6b732b32f46 --- /dev/null +++ b/packages/apps/connector-github/vitest.config.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + // A late console.* must not redden a green suite: the boot test starts a + // whole kernel, whose logger can write after teardown. Mechanism and + // measured costs: examples/app-showcase/vitest.config.ts; enforced + // repo-wide by scripts/check-console-intercept-disarm.mjs. + disableConsoleIntercept: true, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aece0002023..035561e3bbe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -426,6 +426,56 @@ importers: specifier: ^4.1.11 version: 4.1.12(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/coverage-v8@4.1.12)(@vitest/ui@4.1.10)(happy-dom@21.0.4)(jsdom@30.1.1(@noble/hashes@2.4.0))(msw@2.15.0(@types/node@26.6.5)(typescript@6.0.3))(vite@8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15(patch_hash=4d1d35da1809be2a945519cb26890e48107810b9b66c6913a6a89abb5ad6a1e6))(yaml@2.9.1)) + packages/apps/connector-github: + dependencies: + '@objectstack/connector-mcp': + specifier: workspace:* + version: link:../../connectors/connector-mcp + '@objectstack/spec': + specifier: workspace:* + version: link:../../spec + devDependencies: + '@objectstack/cli': + specifier: workspace:* + version: link:../../cli + '@objectstack/verify': + specifier: workspace:* + version: link:../../verify + '@types/node': + specifier: ^26.6.5 + version: 26.6.5 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.11 + version: 4.1.12(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/coverage-v8@4.1.12)(@vitest/ui@4.1.10)(happy-dom@21.0.4)(jsdom@30.1.1(@noble/hashes@2.4.0))(msw@2.15.0(@types/node@26.6.5)(typescript@6.0.3))(vite@8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15(patch_hash=4d1d35da1809be2a945519cb26890e48107810b9b66c6913a6a89abb5ad6a1e6))(yaml@2.9.1)) + + packages/apps/connector-stripe: + dependencies: + '@objectstack/connector-openapi': + specifier: workspace:* + version: link:../../connectors/connector-openapi + '@objectstack/spec': + specifier: workspace:* + version: link:../../spec + devDependencies: + '@objectstack/cli': + specifier: workspace:* + version: link:../../cli + '@objectstack/verify': + specifier: workspace:* + version: link:../../verify + '@types/node': + specifier: ^26.6.5 + version: 26.6.5 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.11 + version: 4.1.12(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/coverage-v8@4.1.12)(@vitest/ui@4.1.10)(happy-dom@21.0.4)(jsdom@30.1.1(@noble/hashes@2.4.0))(msw@2.15.0(@types/node@26.6.5)(typescript@6.0.3))(vite@8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15(patch_hash=4d1d35da1809be2a945519cb26890e48107810b9b66c6913a6a89abb5ad6a1e6))(yaml@2.9.1)) + packages/apps/setup: dependencies: '@objectstack/platform-objects': From 916355ef0eeb789135def2c46767ffbbf553ce98 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 03:23:56 +0000 Subject: [PATCH 2/3] feat(apps): connector-github runs os validate in its test script and pins the unset-credential run ESM package (import.meta under NodeNext); the README states what an under-scoped classic token does, as measured on both credential paths. The lockfile drops the Stripe importer: that app waits on a decision. Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U Co-authored-by: Claude --- packages/apps/connector-github/README.md | 10 +++++--- packages/apps/connector-github/package.json | 3 ++- .../apps/connector-github/test/boot.test.ts | 12 +++++++++ pnpm-lock.yaml | 25 ------------------- 4 files changed, 21 insertions(+), 29 deletions(-) diff --git a/packages/apps/connector-github/README.md b/packages/apps/connector-github/README.md index 8b1e08cbf70..647963f0a72 100644 --- a/packages/apps/connector-github/README.md +++ b/packages/apps/connector-github/README.md @@ -34,9 +34,13 @@ install dialog right after installing. An environment variable named `GITHUB_MCP_TOKEN` on the server takes precedence over a stored value. Prefer a fine-grained token. With a classic token, GitHub's server hides every -tool whose scope the token lacks; if it hides `search_issues` or `issue_read`, -the connector refuses to start rather than quietly running without them, and -the reason names the missing tool. +tool whose scope the token lacks. If that hides `search_issues` or +`issue_read`, the connector does not start without them: a value set in +**Connector Credentials** leaves `github_mcp` disconnected with a reason naming +the missing tools, and a value set as the server environment variable stops the +server from starting at all. Replace the token with one that carries the `repo` +scope: a new value in **Connector Credentials** takes effect at once, a new +environment variable at the next start. The token is only ever sent to GitHub. It is never stored in the app's metadata. diff --git a/packages/apps/connector-github/package.json b/packages/apps/connector-github/package.json index 8eb62bb2623..27dad6a26fd 100644 --- a/packages/apps/connector-github/package.json +++ b/packages/apps/connector-github/package.json @@ -4,6 +4,7 @@ "description": "GitHub connector app (open edition): GitHub's remote MCP server as a declarative `mcp` connector instance with a read-tool allowlist, authenticated by a personal access token the administrator supplies through the credential store.", "license": "Apache-2.0", "private": true, + "type": "module", "main": "./objectstack.config.ts", "types": "./objectstack.config.ts", "exports": { @@ -14,7 +15,7 @@ "build": "objectstack build", "validate": "objectstack validate", "typecheck": "tsc --noEmit", - "test": "vitest run" + "test": "objectstack validate && vitest run" }, "dependencies": { "@objectstack/connector-mcp": "workspace:*", diff --git a/packages/apps/connector-github/test/boot.test.ts b/packages/apps/connector-github/test/boot.test.ts index de91edd5855..4bd03bf1aa6 100644 --- a/packages/apps/connector-github/test/boot.test.ts +++ b/packages/apps/connector-github/test/boot.test.ts @@ -100,6 +100,18 @@ describe('connector-github app — boot against a fake MCP server', () => { expect(transports).toEqual([]); }); + it('a flow run while the credential is unset fails, naming the credential, and dials nothing', async () => { + const res = await booted.apiAs(token, 'POST', '/automation/github_search_issues/trigger', { + params: { query: 'repo:acme/app is:open' }, + }); + expect(res.status).toBe(400); + const body = (await res.json()) as { error?: { code?: string; message?: string } }; + expect(body.error?.code).toBe('FLOW_FAILED'); + expect(body.error?.message).toContain(REF); + expect(calls).toEqual([]); + expect(transports).toEqual([]); + }); + it('once the administrator sets the credential, it materializes with only the included read tools', async () => { const put = await booted.apiAs(token, 'PUT', `/automation/connectors/credential-refs/${REF}`, { value: FIXTURE_TOKEN }); expect(put.status, await put.clone().text()).toBeLessThan(300); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 035561e3bbe..3a9c2d446e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -451,31 +451,6 @@ importers: specifier: ^4.1.11 version: 4.1.12(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/coverage-v8@4.1.12)(@vitest/ui@4.1.10)(happy-dom@21.0.4)(jsdom@30.1.1(@noble/hashes@2.4.0))(msw@2.15.0(@types/node@26.6.5)(typescript@6.0.3))(vite@8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15(patch_hash=4d1d35da1809be2a945519cb26890e48107810b9b66c6913a6a89abb5ad6a1e6))(yaml@2.9.1)) - packages/apps/connector-stripe: - dependencies: - '@objectstack/connector-openapi': - specifier: workspace:* - version: link:../../connectors/connector-openapi - '@objectstack/spec': - specifier: workspace:* - version: link:../../spec - devDependencies: - '@objectstack/cli': - specifier: workspace:* - version: link:../../cli - '@objectstack/verify': - specifier: workspace:* - version: link:../../verify - '@types/node': - specifier: ^26.6.5 - version: 26.6.5 - typescript: - specifier: ^6.0.3 - version: 6.0.3 - vitest: - specifier: ^4.1.11 - version: 4.1.12(@opentelemetry/api@1.9.1)(@types/node@26.6.5)(@vitest/coverage-v8@4.1.12)(@vitest/ui@4.1.10)(happy-dom@21.0.4)(jsdom@30.1.1(@noble/hashes@2.4.0))(msw@2.15.0(@types/node@26.6.5)(typescript@6.0.3))(vite@8.3.5(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15(patch_hash=4d1d35da1809be2a945519cb26890e48107810b9b66c6913a6a89abb5ad6a1e6))(yaml@2.9.1)) - packages/apps/setup: dependencies: '@objectstack/platform-objects': From cf30e616111bfe1b44a6a1a5bf203319ba8f33cf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 03:47:59 +0000 Subject: [PATCH 3/3] test(apps): connector-github tests resolve workspace packages to source and quiet the registry log check:test-source-alias and check:registry-log-declared, both red on the first gate run, both green after. Claude-Session: https://claude.ai/code/session_017xRBcVBKAFk1ntXSjvPt7U Co-authored-by: Claude --- .../apps/connector-github/vitest.config.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/apps/connector-github/vitest.config.ts b/packages/apps/connector-github/vitest.config.ts index 6b732b32f46..a0367924c0c 100644 --- a/packages/apps/connector-github/vitest.config.ts +++ b/packages/apps/connector-github/vitest.config.ts @@ -1,6 +1,10 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. import { defineConfig } from 'vitest/config'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); export default defineConfig({ test: { @@ -9,5 +13,22 @@ export default defineConfig({ // measured costs: examples/app-showcase/vitest.config.ts; enforced // repo-wide by scripts/check-console-intercept-disarm.mjs. disableConsoleIntercept: true, + // Quiet the registry's per-item registration chatter — the engine's own + // `OS_REGISTRY_LOG` seam, not a change to its shipped default. Enforced by + // scripts/check-registry-log-declared.mjs. + env: { OS_REGISTRY_LOG: 'warn' }, + }, + resolve: { + // Every workspace package these tests import resolves to its SOURCE, so a + // verdict here is about the checkout, never about whatever `dist/` was last + // built (`pnpm check:test-source-alias`). Anchored, array form: a bare + // string key matches by prefix and would swallow the subpaths. + alias: [ + { find: /^@objectstack\/connector-mcp$/, replacement: path.resolve(HERE, '../../connectors/connector-mcp/src/index.ts') }, + { find: /^@objectstack\/spec$/, replacement: path.resolve(HERE, '../../spec/src/index.ts') }, + { find: /^@objectstack\/spec\/integration$/, replacement: path.resolve(HERE, '../../spec/src/integration/index.ts') }, + { find: /^@objectstack\/spec\/marketplace$/, replacement: path.resolve(HERE, '../../spec/src/marketplace/index.ts') }, + { find: /^@objectstack\/verify$/, replacement: path.resolve(HERE, '../../verify/src/index.ts') }, + ], }, });