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
35 changes: 18 additions & 17 deletions EXPLAINME.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ This is the Model-Controller-Processor pattern: cartridges are pluggable service

=== Claim 1: Cartridge Orchestration Via Auto-Discovery

**Location**: `/var/mnt/eclipse/repos/boj-server/mcp-bridge/lib/cartridge-loader.ts` (TypeScript cartridge discovery and initialization)
**Location**: `mcp-bridge/lib/cartridge-loader.ts` (TypeScript cartridge discovery and initialization)

**How verified**: The cartridge loader scans `cartridges/*/manifest.json`, reads tool schemas from each, and registers them dynamically with the MCP server. README (§Features) claims "50+ open-source cartridges." The loader validates each manifest, checks for required `name`, `version`, `tools` fields, and prevents duplicate tool names. This enables the "unified endpoint" claim: a single MCP server exposes the union of all cartridges' tools without hardcoding each one.

**Caveat**: Auto-discovery is runtime dynamic; there is no compile-time verification that all cartridge schemas are valid JSON Schema. A malformed manifest will error at MCP startup, not build time.

=== Claim 2: PanLL Grid Layout Auto-Wiring for Panel Cartridges

**Location**: `/var/mnt/eclipse/repos/boj-server/panll/lib/autowire.ts` (ReScript panel autowiring with constraint solver)
**Location**: `panll/lib/autowire.ts` (ReScript panel autowiring with constraint solver)

**How verified**: The PanLL framework defines panels (UI widgets) with declared dependencies. The autowire module runs a topological sort + constraint satisfaction solver to bind panel inputs to outputs from other panels. README's panll/ subdir documents the "workspace layer" that orchestrates 108 panels into coherent layouts. The solver validates connectivity before rendering and rejects cycles.

Expand All @@ -42,25 +42,26 @@ Cartridge pattern is reused in echidna (prover orchestration), gossamer (window
|===
| Path | What's There

| `mcp-bridge/lib/server.ts` | MCP server entry point; listens on stdio for Claude Code
| `mcp-bridge/lib/cartridge-loader.ts` | Runtime cartridge discovery from manifests; dynamic tool registration
| `mcp-bridge/lib/tool-mapper.ts` | Maps MCP tool calls to cartridge method invocations
| `cartridges/*/manifest.json` | Cartridge metadata: name, version, tool schemas, credential requirements
| `cartridges/github-api-mcp/lib/index.ts` | GitHub cartridge: repos, issues, PRs, code search via Octokit
| `cartridges/slack-mcp/lib/index.ts` | Slack cartridge: messaging, channel management via bolt.js
| `cartridges/cloudflare-mcp/lib/index.ts` | Cloudflare cartridge: DNS, Workers, KV, R2, D1 via Official SDK
| `panll/lib/autowire.ts` | Panel grid layout solver; validates connectivity and prevents cycles
| `panll/panels/*/manifest.json` | Panel declarations: inputs, outputs, size, dependencies
| `lib/server.ts` | Elixir server entry point (port 7700); exposes REST API + cart ridge lifecycle
| `lib/cartridge-manager.ex` | Elixir cartridge lifecycle: loading, initialization, error handling, credential validation
| `mcp-bridge/main.js` | MCP server entry point; JSON-RPC stdio transport for Claude Code
| `mcp-bridge/lib/security.js` | Prompt injection detection, rate limiting, input validation, error sanitization
| `mcp-bridge/lib/api-clients.js` | GitHub, GitLab API passthroughs and BoJ REST API wrappers
| `mcp-bridge/lib/tools.js` | MCP tool schema definitions for all cartridge tools
| `mcp-bridge/lib/logger.js` | Structured JSON logging to stderr
| `mcp-bridge/lib/version.js` | Single source of truth for server name and version
| `mcp-bridge/lib/offline-menu.js` | Static cartridge manifest for offline/inspection mode
| `cartridges/*/` | 96 cartridge directories, each with abi/, ffi/, adapter/ structure
| `src/abi/Boj/` | Idris2 ABI definitions (Protocol, Domain, Catalogue, Safety, etc.)
| `panll/` | ReScript/TEA panel framework for UI workspace layer
|===

== Testing Critical Paths

* **Cartridge loading**: `tests/cartridge-loader.test.ts` — validates manifest parsing, schema validation, duplicate detection
* **Tool dispatch**: `tests/tool-mapper.test.ts` — mocks cartridge methods, verifies correct routing from MCP calls
* **Panel autowiring**: `tests/panel-autowire.test.ts` — constraint solver correctness with cyclic graph detection
* **Integration**: `tests/integration/` — end-to-end MCP server ↔ cartridge invocation with real credentials (dry-run mode)
* **Security module**: `tests/security_test.js` — injection detection, unicode bypass prevention, rate limiting, input validation
* **Smoke tests**: `tests/smoke_test.ts` — CLI, MCP protocol, health check, cartridge schemas
* **E2E tests**: `tests/e2e_mcp_test.ts` — MCP server lifecycle, tool invocation, error handling
* **Property tests**: `tests/p2p_cartridge_properties_test.ts` — cartridge invariants, schema compliance
* **Aspect security**: `tests/aspect_security_test.ts` — injection, sandboxing, credential handling, SSRF prevention
* **Integration**: `tests/integration.sh` — end-to-end MCP server ↔ cartridge invocation

== Questions?

Expand Down
2 changes: 1 addition & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import? "contractile.just"

# Project metadata — customize these
project := "Bundle of Joy Server"
version := "0.1.0"
version := "0.3.1"
tier := "infrastructure" # 1 | 2 | infrastructure

# ═══════════════════════════════════════════════════════════════════════════════
Expand Down
4 changes: 2 additions & 2 deletions TOPOLOGY.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: PMPL-1.0-or-later
# TOPOLOGY.md — BoJ Server Component Matrix
#
# Auto-generated 2026-03-29. 92 cartridges across 6 tiers.
# Auto-generated 2026-04-09. 96 cartridges across 6 tiers.

## Ports

Expand All @@ -12,7 +12,7 @@
| 7702 | GraphQL | Running |
| 7703 | SSE (Server-Sent Events) | Running |

## Cartridge Matrix (92 total)
## Cartridge Matrix (96 total)

### Tier 1 — High-Value APIs (11)
| Cartridge | Domain |
Expand Down
24 changes: 24 additions & 0 deletions mcp-bridge/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"env": {
"node": true,
"es2022": true
},
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"rules": {
"no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
"no-undef": "error",
"no-var": "error",
"prefer-const": "warn",
"eqeqeq": ["error", "always"],
"no-eval": "error",
"no-implied-eval": "error",
"no-new-func": "error",
"no-return-await": "warn",
"no-throw-literal": "error",
"no-shadow": "warn",
"curly": ["warn", "multi-line"]
}
}
284 changes: 284 additions & 0 deletions mcp-bridge/lib/api-clients.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
//
// BoJ Server — API client module
//
// Direct passthrough clients for GitHub and GitLab APIs, plus
// BoJ REST API wrappers for cartridge operations.

import { isValidCartridgeName } from "./security.js";
import { warn } from "./logger.js";
import { SERVER_VERSION } from "./version.js";

const BOJ_BASE = process.env.BOJ_URL || "http://localhost:7700";
const GITHUB_TOKEN = process.env.GITHUB_TOKEN || "";
const GITLAB_TOKEN = process.env.GITLAB_TOKEN || "";

// ===================================================================
// BoJ REST API wrappers
// ===================================================================

/** @returns {Promise<object>} */
async function fetchHealth() {
try {
const res = await fetch(`${BOJ_BASE}/health`);
return await res.json();
} catch {
return { status: "offline", message: "BoJ REST API not reachable. Start the server with: systemctl --user start boj-server" };
}
}

/** @returns {Promise<object>} */
async function fetchMenu() {
try {
const res = await fetch(`${BOJ_BASE}/menu`);
return await res.json();
} catch {
warn("BoJ REST API unreachable, using offline menu");
const { OFFLINE_MENU } = await import("./offline-menu.js");
return OFFLINE_MENU;
}
}

/** @returns {Promise<object>} */
async function fetchCartridges() {
try {
const res = await fetch(`${BOJ_BASE}/cartridges`);
return await res.json();
} catch {
const { OFFLINE_MENU } = await import("./offline-menu.js");
return {
note: "Offline mode — cartridge matrix available when BoJ REST API is running",
cartridges: Object.keys(
OFFLINE_MENU.tier_teranga
.concat(OFFLINE_MENU.tier_shield)
.reduce((acc, c) => { acc[c.name] = c.domain; return acc; }, {})
),
};
}
}

/**
* @param {string} name
* @param {object} [params]
* @returns {Promise<object>}
*/
async function invokeCartridge(name, params) {
if (!isValidCartridgeName(name)) {
return { error: `Invalid cartridge name: ${name}` };
}
try {
const res = await fetch(`${BOJ_BASE}/cartridge/${encodeURIComponent(name)}/invoke`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(params || {}),
});
return await res.json();
} catch {
return { error: "BoJ REST API not reachable. Invocation requires a running server.", cartridge: name, hint: "Start with: systemctl --user start boj-server" };
}
}

/**
* @param {string} name
* @returns {Promise<object>}
*/
async function fetchCartridgeInfo(name) {
if (!isValidCartridgeName(name)) {
return { error: `Invalid cartridge name: ${name}` };
}
try {
const res = await fetch(`${BOJ_BASE}/cartridge/${encodeURIComponent(name)}`);
return await res.json();
} catch {
const { OFFLINE_MENU } = await import("./offline-menu.js");
const all = OFFLINE_MENU.tier_teranga.concat(OFFLINE_MENU.tier_shield);
const found = all.find(c => c.name === name);
return found || { error: `Unknown cartridge: ${name}` };
}
}

// ===================================================================
// GitHub API
// ===================================================================

/**
* @param {string} method
* @param {string} path
* @param {object} [body]
* @returns {Promise<object>}
*/
async function githubApiCall(method, path, body) {
if (!GITHUB_TOKEN) {
return { error: "GITHUB_TOKEN not set. Store in vault-mcp or export to environment." };
}
try {
const url = `https://api.github.com${path}`;
const opts = {
method,
headers: {
"Authorization": `Bearer ${GITHUB_TOKEN}`,
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": `boj-server/${SERVER_VERSION}`,
},
};
if (body && method !== "GET") {
opts.headers["Content-Type"] = "application/json";
opts.body = JSON.stringify(body);
}
const res = await fetch(url, opts);
const data = await res.json();
const rateLimit = {
remaining: res.headers.get("x-ratelimit-remaining"),
reset: res.headers.get("x-ratelimit-reset"),
limit: res.headers.get("x-ratelimit-limit"),
};
return { status: res.status, data, rateLimit };
} catch (err) {
return { error: `GitHub API error: ${err.message}` };
}
}

/**
* @param {string} query
* @param {object} [variables]
* @returns {Promise<object>}
*/
async function githubGraphQL(query, variables) {
if (!GITHUB_TOKEN) {
return { error: "GITHUB_TOKEN not set." };
}
try {
const res = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: {
"Authorization": `Bearer ${GITHUB_TOKEN}`,
"Content-Type": "application/json",
"User-Agent": `boj-server/${SERVER_VERSION}`,
},
body: JSON.stringify({ query, variables: variables || {} }),
});
return await res.json();
} catch (err) {
return { error: `GitHub GraphQL error: ${err.message}` };
}
}

/**
* Route GitHub tool calls to real API.
* @param {string} toolName
* @param {Record<string, any>} args
* @returns {Promise<object>}
*/
async function handleGitHubTool(toolName, args) {
switch (toolName) {
case "boj_github_list_repos":
return githubApiCall("GET", `/user/repos?per_page=${args.per_page || 30}&sort=${args.sort || "updated"}`);
case "boj_github_get_repo":
return githubApiCall("GET", `/repos/${args.owner}/${args.repo}`);
case "boj_github_create_issue":
return githubApiCall("POST", `/repos/${args.owner}/${args.repo}/issues`, { title: args.title, body: args.body, labels: args.labels });
case "boj_github_list_issues":
return githubApiCall("GET", `/repos/${args.owner}/${args.repo}/issues?state=${args.state || "open"}&per_page=${args.per_page || 30}`);
case "boj_github_get_issue":
return githubApiCall("GET", `/repos/${args.owner}/${args.repo}/issues/${args.issue_number}`);
case "boj_github_comment_issue":
return githubApiCall("POST", `/repos/${args.owner}/${args.repo}/issues/${args.issue_number}/comments`, { body: args.body });
case "boj_github_create_pr":
return githubApiCall("POST", `/repos/${args.owner}/${args.repo}/pulls`, { title: args.title, body: args.body, head: args.head, base: args.base || "main" });
case "boj_github_list_prs":
return githubApiCall("GET", `/repos/${args.owner}/${args.repo}/pulls?state=${args.state || "open"}`);
case "boj_github_get_pr":
return githubApiCall("GET", `/repos/${args.owner}/${args.repo}/pulls/${args.pull_number}`);
case "boj_github_merge_pr":
return githubApiCall("PUT", `/repos/${args.owner}/${args.repo}/pulls/${args.pull_number}/merge`, { merge_method: args.method || "merge" });
case "boj_github_search_code":
return githubApiCall("GET", `/search/code?q=${encodeURIComponent(args.query)}`);
case "boj_github_search_issues":
return githubApiCall("GET", `/search/issues?q=${encodeURIComponent(args.query)}`);
case "boj_github_get_file":
return githubApiCall("GET", `/repos/${args.owner}/${args.repo}/contents/${args.path}?ref=${args.ref || "main"}`);
case "boj_github_graphql":
return githubGraphQL(args.query, args.variables);
default:
return { error: `Unknown GitHub tool: ${toolName}` };
}
}

// ===================================================================
// GitLab API
// ===================================================================

/**
* @param {string} method
* @param {string} path
* @param {object} [body]
* @returns {Promise<object>}
*/
async function gitlabApiCall(method, path, body) {
if (!GITLAB_TOKEN) {
return { error: "GITLAB_TOKEN not set." };
}
const baseUrl = process.env.GITLAB_URL || "https://gitlab.com";
try {
const url = `${baseUrl}/api/v4${path}`;
const opts = {
method,
headers: {
"PRIVATE-TOKEN": GITLAB_TOKEN,
"Accept": "application/json",
"User-Agent": `boj-server/${SERVER_VERSION}`,
},
};
if (body && method !== "GET") {
opts.headers["Content-Type"] = "application/json";
opts.body = JSON.stringify(body);
}
const res = await fetch(url, opts);
const data = await res.json();
return { status: res.status, data };
} catch (err) {
return { error: `GitLab API error: ${err.message}` };
}
}

/**
* Route GitLab tool calls to real API.
* @param {string} toolName
* @param {Record<string, any>} args
* @returns {Promise<object>}
*/
async function handleGitLabTool(toolName, args) {
switch (toolName) {
case "boj_gitlab_list_projects":
return gitlabApiCall("GET", `/projects?owned=true&per_page=${args.per_page || 20}`);
case "boj_gitlab_get_project":
return gitlabApiCall("GET", `/projects/${encodeURIComponent(args.project_id)}`);
case "boj_gitlab_create_issue":
return gitlabApiCall("POST", `/projects/${encodeURIComponent(args.project_id)}/issues`, { title: args.title, description: args.description });
case "boj_gitlab_list_issues":
return gitlabApiCall("GET", `/projects/${encodeURIComponent(args.project_id)}/issues?state=${args.state || "opened"}`);
case "boj_gitlab_create_mr":
return gitlabApiCall("POST", `/projects/${encodeURIComponent(args.project_id)}/merge_requests`, { title: args.title, source_branch: args.source, target_branch: args.target || "main" });
case "boj_gitlab_list_mrs":
return gitlabApiCall("GET", `/projects/${encodeURIComponent(args.project_id)}/merge_requests?state=${args.state || "opened"}`);
case "boj_gitlab_list_pipelines":
return gitlabApiCall("GET", `/projects/${encodeURIComponent(args.project_id)}/pipelines`);
case "boj_gitlab_setup_mirror":
return gitlabApiCall("POST", `/projects/${encodeURIComponent(args.project_id)}/remote_mirrors`, { url: args.target_url, enabled: true });
default:
return { error: `Unknown GitLab tool: ${toolName}` };
}
}

export {
BOJ_BASE,
fetchCartridgeInfo,
fetchCartridges,
fetchHealth,
fetchMenu,
handleGitHubTool,
handleGitLabTool,
invokeCartridge,
};
Loading
Loading