Skip to content
Open
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
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ jobs:
validate:
name: Validate
runs-on: runs-on=${{ github.run_id }}/runner=evertrust
env:
HORIZON_E2E_URL: ${{ secrets.HORIZON_E2E_URL }}
HORIZON_E2E_API_ID: ${{ secrets.HORIZON_E2E_API_ID }}
HORIZON_E2E_API_KEY: ${{ secrets.HORIZON_E2E_API_KEY }}
steps:
- name: Configure Runs-On
uses: runs-on/action@v2
Expand Down Expand Up @@ -84,9 +88,5 @@ jobs:
run: bun run test:scenarios

- name: QA E2E smoke
if: ${{ secrets.HORIZON_E2E_URL != '' && secrets.HORIZON_E2E_API_ID != '' && secrets.HORIZON_E2E_API_KEY != '' }}
env:
HORIZON_E2E_URL: ${{ secrets.HORIZON_E2E_URL }}
HORIZON_E2E_API_ID: ${{ secrets.HORIZON_E2E_API_ID }}
HORIZON_E2E_API_KEY: ${{ secrets.HORIZON_E2E_API_KEY }}
if: ${{ env.HORIZON_E2E_URL != '' && env.HORIZON_E2E_API_ID != '' && env.HORIZON_E2E_API_KEY != '' }}
run: bun run test:e2e:smoke
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ htmlcov/
.playwright-mcp/
audit/
.playwright-cli/
output/
15 changes: 15 additions & 0 deletions .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"config": {
"default": true,
"MD013": false,
"MD024": { "siblings_only": true },
"MD029": false,
"MD031": false,
"MD033": false,
"MD034": false,
"MD040": false,
"MD041": false,
"MD046": false
},
"ignores": ["node_modules/**", "dist/**", ".gitnexus/**"]
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ All mutating tools include a STOP confirmation block that instructs the LLM to a

### Prerequisites

- Bun 1.x+ (recommended) or Node.js 24.10+
- Bun 1.x+ (recommended) or Node.js >=24.10
- An Evertrust Horizon instance (tested on 2.8, expected to work on 2.7 and 2.9)
- API credentials or a client certificate with appropriate permissions

Expand Down
146 changes: 144 additions & 2 deletions bun.lock

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions docs/client-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,19 @@ HORIZON_API_ID = "your-api-id"
HORIZON_API_KEY = "your-api-key"
```

Or with a local source checkout:

```toml
[mcp_servers.horizon]
command = "node"
args = ["/absolute/path/to/horizon-mcp/dist/index.js"]

[mcp_servers.horizon.env]
HORIZON_URL = "https://horizon.example.com"
HORIZON_API_ID = "your-api-id"
HORIZON_API_KEY = "your-api-key"
```

In the **Codex Desktop app**, you can also add the server through **Settings > MCP** and follow the GUI prompts.

Alternatively, add via the CLI:
Expand Down
9 changes: 8 additions & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Prerequisites

- Bun 1.x+ (or Node.js 20+)
- Bun 1.x+ (recommended) or Node.js >=24.10
- An Evertrust Horizon instance (tested on 2.8, expected to work on 2.7 and 2.9)
- API credentials or a client certificate with appropriate permissions

Expand All @@ -28,6 +28,13 @@ Then run:
horizon-mcp-server
```

### Local install

```bash
npm install
npm run build
```

### Standalone binary

Download the pre-built binary for your platform from the [releases page](https://github.com/evertrust/horizon-mcp/releases), then:
Expand Down
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"horizon-mcp": "./dist/index.js"
},
"files": [
"dist/"
"dist/",
"skills/"
],
"publishConfig": {
"registry": "https://npm.pkg.github.com",
Expand All @@ -26,10 +27,12 @@
"test:e2e:smoke": "vitest run --config vitest.config.e2e.ts tests/e2e/docs.test.ts --reporter=verbose && vitest run --config vitest.config.e2e.ts tests/e2e/horizon.test.ts -t \"exports events as CSV\" --reporter=verbose",
"test:scenarios": "vitest run --config vitest.config.llm.ts --reporter=verbose",
"test:llm": "bun run test:scenarios",
"validate:ci": "bun run format:check && bun run lint && bun run typecheck && bun run build && bun run verify:truth && bun run docs:diff && bun run test && bun run test:scenarios",
"validate:ci": "bun run format:check && bun run lint && bun run typecheck && bun run build && bun run verify:truth && bun run verify:skills && bun run lint:skills && bun run docs:diff && bun run test && bun run test:scenarios",
"docs:refresh": "bun scripts/refresh-docs.ts",
"docs:diff": "bun scripts/diff-docs.ts",
"verify:truth": "bun scripts/verify-truth.ts",
"verify:skills": "bun scripts/verify-skills.ts",
"lint:skills": "markdownlint-cli2 \"skills/**/*.md\"",
"lint": "eslint src/ tests/",
"lint:fix": "eslint src/ tests/ --fix",
"format": "prettier --write src/ tests/",
Expand Down Expand Up @@ -60,12 +63,14 @@
"globals": "17.4.0",
"husky": "9.1.7",
"lint-staged": "16.4.0",
"markdownlint-cli2": "0.18.1",
"prettier": "3.8.1",
"semantic-release": "^25.0.3",
"tsup": "8.5.1",
"tsx": "4.21.0",
"typescript": "6.0.2",
"typescript-eslint": "8.58.0",
"vitest": "4.1.2"
"vitest": "4.1.2",
"yaml": "2.7.0"
}
}
17 changes: 11 additions & 6 deletions scripts/lib/docs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import vm from 'node:vm';
import { format } from 'prettier';

import type {
DocPage,
Expand Down Expand Up @@ -311,9 +312,10 @@ function buildDocVersionCatalog(
};
}

function writeJson(path: string, data: unknown): void {
async function writeJson(path: string, data: unknown): Promise<void> {
mkdirSync(resolve(path, '..'), { recursive: true });
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
const json = await format(JSON.stringify(data), { parser: 'json' });
writeFileSync(path, json, 'utf8');
}

function parseSitemapLocs(xml: string): string[] {
Expand Down Expand Up @@ -683,16 +685,19 @@ export async function buildDocsArtifacts(): Promise<{

export async function writeDocsArtifacts(): Promise<void> {
const artifacts = await buildDocsArtifacts();
writeJson(
await writeJson(
join(OUTPUT_DIR, 'product-doc-pages.json'),
artifacts.productCatalog,
);
writeJson(join(OUTPUT_DIR, 'api-doc-pages.json'), artifacts.apiCatalog);
writeJson(
await writeJson(join(OUTPUT_DIR, 'api-doc-pages.json'), artifacts.apiCatalog);
await writeJson(
join(OUTPUT_DIR, 'companion-doc-pages.json'),
artifacts.companionCatalog,
);
writeJson(join(OUTPUT_DIR, 'doc-versions.json'), artifacts.versionCatalog);
await writeJson(
join(OUTPUT_DIR, 'doc-versions.json'),
artifacts.versionCatalog,
);
}

function normalizeArtifact(value: unknown): unknown {
Expand Down
93 changes: 72 additions & 21 deletions scripts/lib/truth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ export interface TruthInputs {
outputDir: string;
}

interface StoredOperationsDocument {
routes?: unknown;
operations?: unknown;
paths?: Record<string, Record<string, unknown>>;
}

function expandHome(pathValue: string): string {
if (!pathValue.startsWith('~/')) {
return pathValue;
Expand Down Expand Up @@ -107,6 +113,7 @@ export function resolveTruthInputs(projectRoot: string): TruthInputs {
[
process.env['HORIZON_SOURCE_ROOT'] ?? '',
'../horizon',
'src/generated/docs/horizon-routes.json',
'/Users/sbo/Documents/EVERTRUST/horizon',
].filter(Boolean),
);
Expand All @@ -115,6 +122,7 @@ export function resolveTruthInputs(projectRoot: string): TruthInputs {
[
process.env['HORIZON_OPENAPI_PATH'] ?? '',
'../evertrust_horizon_openapi.json',
'src/generated/docs/openapi-operations.json',
'/Users/sbo/Downloads/evertrust_horizon_openapi.json',
].filter(Boolean),
);
Expand Down Expand Up @@ -150,6 +158,55 @@ function readText(path: string): string {
return readFileSync(path, 'utf8');
}

function isNormalizedOperation(value: unknown): value is NormalizedOperation {
return (
Boolean(value) &&
typeof value === 'object' &&
typeof (value as NormalizedOperation).method === 'string' &&
typeof (value as NormalizedOperation).path === 'string' &&
typeof (value as NormalizedOperation).sourceFile === 'string'
);
}

function sortOperations(
operations: readonly NormalizedOperation[],
): NormalizedOperation[] {
return [...operations].sort((left, right) => {
const pathOrder = left.path.localeCompare(right.path);
if (pathOrder !== 0) {
return pathOrder;
}
const methodOrder = left.method.localeCompare(right.method);
if (methodOrder !== 0) {
return methodOrder;
}
return left.sourceFile.localeCompare(right.sourceFile);
});
}

function readStoredOperations(
inputPath: string,
field: 'routes' | 'operations',
): NormalizedOperation[] | undefined {
if (!inputPath.endsWith('.json')) {
return undefined;
}

const document = JSON.parse(readText(inputPath)) as StoredOperationsDocument;
const operations = document[field];
if (!Array.isArray(operations)) {
return undefined;
}

return sortOperations(
operations.filter(isNormalizedOperation).map((operation) => ({
method: operation.method,
path: normalizeRoutePath(operation.path),
sourceFile: operation.sourceFile,
})),
);
}

function lineNumberAt(
sourceFile: ts.SourceFile,
nodeOrIndex: ts.Node | number,
Expand Down Expand Up @@ -293,6 +350,11 @@ export function collectHorizonOperations(
horizonRoot: string,
projectRoot = process.cwd(),
): NormalizedOperation[] {
const storedOperations = readStoredOperations(horizonRoot, 'routes');
if (storedOperations) {
return storedOperations;
}

const operations: NormalizedOperation[] = [];

for (const route of parseRootRoutes(horizonRoot)) {
Expand All @@ -317,26 +379,21 @@ export function collectHorizonOperations(
}
}

return operations.sort((left, right) => {
const pathOrder = left.path.localeCompare(right.path);
if (pathOrder !== 0) {
return pathOrder;
}
const methodOrder = left.method.localeCompare(right.method);
if (methodOrder !== 0) {
return methodOrder;
}
return left.sourceFile.localeCompare(right.sourceFile);
});
return sortOperations(operations);
}

export function collectOpenApiOperations(
openApiPath: string,
projectRoot = process.cwd(),
): NormalizedOperation[] {
const document = JSON.parse(readText(openApiPath)) as {
paths?: Record<string, Record<string, unknown>>;
};
const storedOperations = readStoredOperations(openApiPath, 'operations');
if (storedOperations) {
return storedOperations;
}

const document = JSON.parse(
readText(openApiPath),
) as StoredOperationsDocument;

const operations: NormalizedOperation[] = [];
const paths = document.paths ?? {};
Expand All @@ -358,13 +415,7 @@ export function collectOpenApiOperations(
}
}

return operations.sort((left, right) => {
const pathOrder = left.path.localeCompare(right.path);
if (pathOrder !== 0) {
return pathOrder;
}
return left.method.localeCompare(right.method);
});
return sortOperations(operations);
}

export function collectMcpPathReferences(
Expand Down
Loading
Loading