Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions .github/workflows/port-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -367,10 +367,32 @@ jobs:
# for the same reason and with the same freshness check. The resolved
# versions are forced so the build neither re-resolves nor rewrites
# Package.resolved.
- if: matrix.port == 'swift'
uses: swift-actions/setup-swift@7ca6abe6b3b0e8b5421b88be48feee39cbf52c6a # v2
- name: Install selected Swift toolchain
if: matrix.port == 'swift'
uses: jdx/mise-action@9149ea85001c7435d5a66bb127d6a1b6227cb0a5 # v5.0.0
env:
MISE_SWIFT_GPG_VERIFY: 'true'
with:
swift-version: ${{ steps.native-version.outputs.version }}
version: '2026.9.17'
install_args: swift@${{ steps.native-version.outputs.version }}
cache: false
env: false
export_path: false
add_shims_to_path: false
- name: Verify selected Swift toolchain
if: matrix.port == 'swift'
env:
SWIFT_VERSION: ${{ steps.native-version.outputs.version }}
run: |
set -euo pipefail
swift_bin=$(mise where "swift@$SWIFT_VERSION")/bin
actual=$("$swift_bin/swift" --version | sed -nE 's/^(Apple )?Swift version ([0-9.]+).*$/\2/p')
[[ "$actual" == "$SWIFT_VERSION" ]] || {
echo "::error::Expected Swift $SWIFT_VERSION, got ${actual:-no version}" >&2
exit 1
}
echo "Swift $actual: $swift_bin"
printf '%s\n' "$swift_bin" >> "$GITHUB_PATH"
- name: Emit the Swift symbol graph
if: matrix.port == 'swift'
working-directory: port
Expand Down
10 changes: 5 additions & 5 deletions scripts/gen-mcp-tools.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ const PORTS = [
{
slug: 'java',
dir: '~/work/libtmux/libtmux-java/libtmux-mcp/src/main/java/io/github/libtmux/mcp',
glob: 'Catalog.java',
// Anchored on `tools.add`, the registration itself, because the catalog
// reaches it through five factories and two decorators. Yields exactly the
// 45 names of `CapabilityRegistryTest.CATALOG_ORDER`.
pattern: /tools\.add\(\s*(?:\w+\(\s*)+"([a-z][a-z0-9_]*)"/gs,
glob: '{Catalog,*Tools}.java',
// Older revisions register in Catalog; newer ones split by toolset and
// qualify factories and decorators with Catalog. Anchor on registration
// so schema arguments and helper calls cannot become tool names.
pattern: /tools\.add\(\s*(?:(?:\w+\.)*\w+\(\s*)+"([a-z][a-z0-9_]*)"/gs,
},
{
slug: 'dotnet',
Expand Down
33 changes: 29 additions & 4 deletions site/test/mcp-source-bound.outer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ const tool = { name: 'list_sessions', description: 'Selected source contract', i
type: 'object', properties: { label: { type: 'string', description: 'Selected source argument' } },
} }

function fixture(slug: 'go' | 'py' | 'ruby', run: (fixture: {
function fixture(slug: 'go' | 'py' | 'ruby' | 'java', run: (fixture: {
directory: string; checkout: string; sha: string; model: string; snapshot: string; catalog: string;
discovery: string; env: NodeJS.ProcessEnv;
invoke: (script: 'protocol' | 'tools', args?: string[], env?: NodeJS.ProcessEnv) => SpawnSyncReturns<string>;
}) => void) {
}) => void, javaLayout: 'catalog' | 'toolsets' = 'catalog') {
const directory = mkdtempSync(join(tmpdir(), 'libtmux-mcp-source-'))
const checkout = join(directory, 'source')
const git = (...args: string[]) => execFileSync('git', ['-C', checkout, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim()
Expand All @@ -31,8 +31,15 @@ function fixture(slug: 'go' | 'py' | 'ruby', run: (fixture: {
copyFileSync(join(root, file), target)
}
write(join(directory, 'package.json'), '{"type":"module"}')
write(join(checkout, slug === 'go' ? 'mcp/manifest_catalog.go' : 'src/libtmux_mcp/tools/sessions.py'),
slug === 'go' ? 'var catalog = []tool{{name: "list_sessions"}}\n' : 'mcp.tool()(list_sessions)\n')
if (slug === 'java') {
const directory = join(checkout, 'libtmux-mcp/src/main/java/io/github/libtmux/mcp')
write(join(directory, javaLayout === 'catalog' ? 'Catalog.java' : 'InspectTools.java'),
`Catalog.tool("helper_only");\ntools.add(${javaLayout === 'catalog' ? 'inspect(' : 'Catalog.literalized(Catalog.tool('}"list_sessions")${javaLayout === 'catalog' ? ')' : '))'};\n`)
write(join(directory, 'CatalogTest.java'), 'tools.add(tool("test_only"));\n')
} else {
write(join(checkout, slug === 'go' ? 'mcp/manifest_catalog.go' : 'src/libtmux_mcp/tools/sessions.py'),
slug === 'go' ? 'var catalog = []tool{{name: "list_sessions"}}\n' : 'mcp.tool()(list_sessions)\n')
}
if (slug === 'py') write(join(checkout, 'docs/tools/list-sessions.md'), '# List sessions\n')
git('init', '-q')
git('add', '.')
Expand Down Expand Up @@ -108,6 +115,24 @@ it.each(['go', 'py'] as const)('binds the %s catalog to its actual MCP source an
})
})

it.each(['catalog', 'toolsets'] as const)('binds Java %s registrations to runtime schemas and their source lines', (layout) => {
fixture('java', ({ sha, catalog, invoke }) => {
const captured = invoke('protocol')
expect(captured.status, captured.stderr).toBe(0)
const generated = invoke('tools')
expect(generated.status, generated.stderr).toBe(0)
const result = JSON.parse(readFileSync(catalog, 'utf8')).ports.java
expect(result.tools).toEqual(['list_sessions'])
expect(result.registrations).toEqual([expect.objectContaining({
...tool, schemaStatus: 'runtime', source: {
repo: 'libtmux/libtmux-java', revision: sha, extractedRevision: sha,
file: `libtmux-mcp/src/main/java/io/github/libtmux/mcp/${layout === 'catalog' ? 'Catalog' : 'InspectTools'}.java`,
line: 2,
},
})])
}, layout)
})

it.each(['core revision', 'MCP revision', 'MCP repository'])('rejects an API model with the wrong %s before starting the server', (field) => {
fixture('go', ({ model, discovery, invoke }) => {
const value = JSON.parse(readFileSync(model, 'utf8'))
Expand Down
24 changes: 24 additions & 0 deletions site/test/port-docs-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,30 @@ describe('selected MCP runtime provisioning', () => {
} finally { rmSync(directory, { recursive: true, force: true }) }
})

it.each(['6.2.4', '6.2.3', 'unrecognized'])('verifies the installed Swift compiler reports %s before adding it to PATH', (version) => {
const directory = mkdtempSync(join(tmpdir(), 'libtmux-swift-version-'))
try {
const bin = join(directory, 'bin')
const output = join(directory, 'path')
mkdirSync(bin)
writeFileSync(output, '')
writeFileSync(join(bin, 'mise'), '#!/bin/sh\nprintf "%s\\n" "$STUB_TOOLCHAIN"\n', { mode: 0o700 })
writeFileSync(join(bin, 'swift'), `#!/bin/sh\nprintf '%s\\n' 'Swift version ${version} (fixture)'\n`, { mode: 0o700 })
const result = spawnSync('bash', ['-e', '-c', script('Verify selected Swift toolchain')], {
cwd: directory, encoding: 'utf8', timeout: 5000,
env: { ...process.env, PATH: `${bin}:${process.env.PATH}`, STUB_TOOLCHAIN: directory, SWIFT_VERSION: '6.2.4', GITHUB_PATH: output },
})
if (version === '6.2.4') {
expect(result.status, result.stderr).toBe(0)
expect(readFileSync(output, 'utf8')).toBe(`${bin}\n`)
} else {
expect(result.status).not.toBe(0)
expect(result.stderr).toContain('Expected Swift 6.2.4')
expect(readFileSync(output, 'utf8')).toBe('')
}
} finally { rmSync(directory, { recursive: true, force: true }) }
})

it('installs the separate Python MCP checkout without altering the captured inputs', () => {
const snapshot = workflow.indexOf('name: Snapshot source inputs before native generation')
const runtime = workflow.indexOf("name: Install Python's separate MCP runtime")
Expand Down
Loading