Skip to content

Commit 1a0e1cc

Browse files
authored
Merge pull request #1222 from getlarge/issue-1209-mcp-apps-ext-apps
refactor(mcp-server): prepare shared MCP app UI foundation
2 parents 1a1ca0d + 94b5a80 commit 1a0e1cc

84 files changed

Lines changed: 4909 additions & 1711 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,7 @@ jobs:
696696
MCP_SERVER_IMAGE: ghcr.io/getlarge/themoltnet/mcp-server:ci-${{ github.sha }}
697697
DB_MIGRATE_IMAGE: ghcr.io/getlarge/themoltnet/db-migrate:ci-${{ github.sha }}
698698
CONSOLE_IMAGE: ghcr.io/getlarge/themoltnet/console:ci-${{ github.sha }}
699+
MCP_HOST_IMAGE: ghcr.io/getlarge/themoltnet/mcp-host:ci-${{ github.sha }}
699700
# Pre-set the URLs the e2e tests need. Without this, Nx's bin/nx.js
700701
# loads workspace-root .env at startup; our .env is dotenvx-encrypted,
701702
# so plain-dotenv pulls ciphertext into DATABASE_URL and pg silently
@@ -762,6 +763,7 @@ jobs:
762763
MCP_SERVER_IMAGE: ghcr.io/getlarge/themoltnet/mcp-server:ci-${{ github.sha }}
763764
DB_MIGRATE_IMAGE: ghcr.io/getlarge/themoltnet/db-migrate:ci-${{ github.sha }}
764765
CONSOLE_IMAGE: ghcr.io/getlarge/themoltnet/console:ci-${{ github.sha }}
766+
MCP_HOST_IMAGE: ghcr.io/getlarge/themoltnet/mcp-host:ci-${{ github.sha }}
765767
steps:
766768
- uses: actions/checkout@v6
767769
- uses: pnpm/action-setup@v5
@@ -801,6 +803,19 @@ jobs:
801803
CONSOLE_BASE_URL: http://localhost:5174
802804
MAILSLURPER_API_URL: http://localhost:4437
803805
run: pnpm exec nx run @moltnet/console:e2e
806+
- name: Run MCP Host Browser E2E tests
807+
env:
808+
BASE_URL: http://127.0.0.1:8082
809+
MCP_SERVER_URL: http://127.0.0.1:8001
810+
REST_API_URL: http://127.0.0.1:8080
811+
DATABASE_URL: postgresql://moltnet:moltnet_secret@127.0.0.1:5433/moltnet
812+
ORY_HYDRA_PUBLIC_URL: http://127.0.0.1:4444
813+
ORY_HYDRA_ADMIN_URL: http://127.0.0.1:4445
814+
ORY_KETO_PUBLIC_URL: http://127.0.0.1:4466
815+
ORY_KETO_ADMIN_URL: http://127.0.0.1:4467
816+
ORY_KRATOS_PUBLIC_URL: http://127.0.0.1:4433
817+
ORY_KRATOS_ADMIN_URL: http://127.0.0.1:4434
818+
run: pnpm exec nx run @moltnet/mcp-host-e2e:e2e
804819
- name: Docker Compose logs
805820
if: failure()
806821
run: docker compose -f docker-compose.e2e.yaml -f docker-compose.e2e.ci.yaml logs --tail=50
@@ -826,6 +841,7 @@ jobs:
826841
MCP_SERVER_IMAGE: ghcr.io/getlarge/themoltnet/mcp-server:ci-${{ github.sha }}
827842
DB_MIGRATE_IMAGE: ghcr.io/getlarge/themoltnet/db-migrate:ci-${{ github.sha }}
828843
CONSOLE_IMAGE: ghcr.io/getlarge/themoltnet/console:ci-${{ github.sha }}
844+
MCP_HOST_IMAGE: ghcr.io/getlarge/themoltnet/mcp-host:ci-${{ github.sha }}
829845
steps:
830846
- uses: actions/checkout@v6
831847
- uses: pnpm/action-setup@v5
@@ -974,6 +990,9 @@ jobs:
974990
- image: console
975991
dockerfile: apps/console/Dockerfile
976992
needed-when: e2e-any
993+
- image: mcp-host
994+
dockerfile: apps/mcp-host/Dockerfile
995+
needed-when: e2e-any
977996
steps:
978997
- name: Skip if image not needed
979998
id: gate

AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ See `libs/database/drizzle/README.md` for the full workflow, rollback strategy,
159159
- **NEVER use `paths` aliases** in any `tsconfig.json` (root or workspace). Package resolution must go through pnpm workspace symlinks and `package.json` `exports`, not TypeScript path mappings.
160160
- **Source exports**: All workspace packages export source directly via `"import": "./src/index.ts"` and `"types": "./src/index.ts"` in conditional exports. The `main` and `types` top-level fields point to `./dist/` as fallback. TypeScript, Vite, and vitest all resolve via the `import` condition to source files. No custom conditions needed.
161161
- **Incremental builds**: Lib packages use `tsc -b` for incremental compilation with `.tsbuildinfo` caching. App packages (server, rest-api, mcp-server) use `vite build` with SSR mode to produce self-contained bundles where workspace deps are inlined and third-party deps stay external. The root `build` script runs `pnpm -r run build` which executes in topological order (libs first, then apps).
162-
- **Project references**: The root `tsconfig.json` is a solution file (`files: []` + `references` to all packages). Each workspace tsconfig has `composite: true`. References are auto-synced from `workspace:*` dependencies by `update-ts-references` (runs in postinstall).
162+
- **Project references**: The root `tsconfig.json` is a solution file (`files: []` + `references` to all packages). Each workspace tsconfig has `composite: true`. References are synced by Nx TypeScript sync; `pnpm install` runs `nx sync` in `postinstall`, and Nx can also validate sync during task execution.
163163
- **Typecheck**: Each workspace runs `tsc -b --emitDeclarationOnly` via `pnpm -r run typecheck`. This emits `.d.ts` + `.tsbuildinfo` to a directory determined by the workspace's group (see "Build cache contract" below), which is required because `composite: true` and project references don't support `--noEmit`.
164164
- **Workspace linking**: `inject-workspace-packages=false` in `.npmrc` — workspace dependencies are symlinked (not hardlinked copies), so changes propagate instantly without re-running `pnpm install`.
165165

@@ -190,7 +190,7 @@ When creating a new `libs/` or `apps/` package:
190190

191191
1. Add a `tsconfig.json` extending root (`"extends": "../../tsconfig.json"`) with `composite: true`, `outDir` and `rootDir`
192192
- For frontend apps with JSX: also add `"jsx": "react-jsx"`, `"lib": ["ES2022", "DOM"]`
193-
- tsconfig `references` are auto-synced by `update-ts-references` on `pnpm install`
193+
- tsconfig `references` are synced by Nx TypeScript sync on `pnpm install` via `nx sync`
194194
2. Set `main`/`types` to `./dist/index.js`/`./dist/index.d.ts` and `exports` with source-direct format:
195195
```json
196196
"exports": { ".": { "import": "./src/index.ts", "types": "./src/index.ts" } }
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import playwright from 'eslint-plugin-playwright';
2+
import baseConfig from '../../eslint.config.mjs';
3+
4+
export default [
5+
playwright.configs['flat/recommended'],
6+
...baseConfig,
7+
{
8+
files: ['**/*.ts', '**/*.js'],
9+
// Override or add rules here
10+
rules: {},
11+
},
12+
];

apps/mcp-host-e2e/global-setup.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
async function waitForHealthy(url: string, maxAttempts = 60): Promise<void> {
2+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
3+
try {
4+
const response = await fetch(url);
5+
if (response.ok) {
6+
return;
7+
}
8+
} catch {
9+
// Service not ready yet.
10+
}
11+
12+
await new Promise((resolve) => {
13+
setTimeout(resolve, 2000);
14+
});
15+
}
16+
17+
throw new Error(
18+
`Service at ${url} did not become healthy after ${maxAttempts} attempts`,
19+
);
20+
}
21+
22+
const E2E_LOCAL_DEFAULTS = {
23+
BASE_URL: 'http://127.0.0.1:8082',
24+
MCP_SERVER_URL: 'http://127.0.0.1:8001',
25+
REST_API_URL: 'http://127.0.0.1:8080',
26+
DATABASE_URL: 'postgresql://moltnet:moltnet_secret@127.0.0.1:5433/moltnet',
27+
ORY_HYDRA_PUBLIC_URL: 'http://127.0.0.1:4444',
28+
ORY_HYDRA_ADMIN_URL: 'http://127.0.0.1:4445',
29+
ORY_KETO_PUBLIC_URL: 'http://127.0.0.1:4466',
30+
ORY_KETO_ADMIN_URL: 'http://127.0.0.1:4467',
31+
ORY_KRATOS_PUBLIC_URL: 'http://127.0.0.1:4433',
32+
ORY_KRATOS_ADMIN_URL: 'http://127.0.0.1:4434',
33+
} as const;
34+
35+
function applyLocalFallbackEnv(): void {
36+
for (const [name, value] of Object.entries(E2E_LOCAL_DEFAULTS)) {
37+
process.env[name] ??= value;
38+
}
39+
}
40+
41+
export default async function globalSetup() {
42+
applyLocalFallbackEnv();
43+
44+
await Promise.all([
45+
waitForHealthy(`${process.env['ORY_KRATOS_PUBLIC_URL']}/health/alive`),
46+
waitForHealthy(`${process.env['ORY_HYDRA_PUBLIC_URL']}/health/alive`),
47+
waitForHealthy(`${process.env['ORY_KETO_PUBLIC_URL']}/health/alive`),
48+
waitForHealthy(`${process.env['REST_API_URL']}/health`),
49+
waitForHealthy(`${process.env['MCP_SERVER_URL']}/healthz`),
50+
waitForHealthy(`${process.env['BASE_URL']}/healthz`),
51+
]);
52+
}

apps/mcp-host-e2e/package.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"name": "@moltnet/mcp-host-e2e",
3+
"version": "0.1.0",
4+
"license": "AGPL-3.0-only",
5+
"private": true,
6+
"type": "module",
7+
"dependencies": {
8+
"@moltnet/mcp-test-harness": "workspace:*"
9+
},
10+
"nx": {
11+
"tags": [
12+
"type:e2e",
13+
"scope:tooling",
14+
"platform:cli"
15+
],
16+
"implicitDependencies": [
17+
"@moltnet/mcp-host"
18+
]
19+
}
20+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { fileURLToPath } from 'node:url';
2+
3+
import { nxE2EPreset } from '@nx/playwright/preset';
4+
import { defineConfig, devices } from '@playwright/test';
5+
6+
export default defineConfig({
7+
...nxE2EPreset(fileURLToPath(import.meta.url), { testDir: './src' }),
8+
globalSetup: './global-setup.ts',
9+
use: {
10+
baseURL: process.env['BASE_URL'] || 'http://127.0.0.1:8082',
11+
trace: 'on-first-retry',
12+
},
13+
projects: [
14+
{
15+
name: 'chromium',
16+
use: { ...devices['Desktop Chrome'] },
17+
},
18+
],
19+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {
2+
createMcpTestHarness,
3+
type McpTestHarness,
4+
} from '@moltnet/mcp-test-harness';
5+
import { expect, test } from '@playwright/test';
6+
7+
let harness: McpTestHarness;
8+
9+
test.beforeAll(async () => {
10+
harness = await createMcpTestHarness();
11+
});
12+
13+
test.afterAll(async () => {
14+
await harness?.teardown();
15+
});
16+
17+
test('loads the tasks MCP app through the host bridge', async ({ page }) => {
18+
const url = new URL('/', 'http://127.0.0.1:8082');
19+
url.searchParams.set('tool', 'tasks_app_open');
20+
url.searchParams.set('autorun', '1');
21+
url.searchParams.set('args', '{}');
22+
url.searchParams.set('server', `${harness.mcpBaseUrl}/mcp`);
23+
url.searchParams.set('clientId', harness.agent.clientId);
24+
url.searchParams.set('clientSecret', harness.agent.clientSecret);
25+
26+
await page.goto(url.toString());
27+
28+
await expect(page.locator('h1')).toContainText('Basic Host Fixture');
29+
await expect(page.locator('#status')).toContainText('completed');
30+
await expect(page.locator('#app-state')).toContainText('app-ready');
31+
await expect(page.locator('#app-frame')).toBeVisible();
32+
await expect(page.locator('#result')).toContainText(
33+
'ui://moltnet/tasks.html',
34+
);
35+
});

apps/mcp-host-e2e/tsconfig.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"compilerOptions": {
3+
"allowJs": true,
4+
"module": "ESNext",
5+
"outDir": "../../dist/out-tsc",
6+
"sourceMap": false
7+
},
8+
"extends": "../../tsconfig.json",
9+
"include": [
10+
"**/*.ts",
11+
"**/*.js",
12+
"playwright.config.ts",
13+
"src/**/*.spec.ts",
14+
"src/**/*.spec.js",
15+
"src/**/*.test.ts",
16+
"src/**/*.test.js",
17+
"src/**/*.d.ts"
18+
],
19+
"references": [
20+
{
21+
"path": "../../libs/mcp-test-harness"
22+
}
23+
]
24+
}

apps/mcp-host/Dockerfile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
FROM node:22.19.0-slim AS base
2+
RUN npm install -g pnpm@10.29.3
3+
WORKDIR /app
4+
5+
FROM base AS deps
6+
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./
7+
COPY apps/mcp-host/package.json apps/mcp-host/
8+
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
9+
pnpm install --frozen-lockfile --ignore-scripts
10+
11+
FROM deps AS build
12+
COPY . .
13+
RUN pnpm --filter @moltnet/mcp-host exec vite build
14+
15+
FROM node:22.19.0-slim AS production
16+
LABEL org.opencontainers.image.source="https://github.com/getlarge/themoltnet"
17+
LABEL org.opencontainers.image.description="MoltNet MCP host e2e fixture"
18+
LABEL org.opencontainers.image.licenses="MIT"
19+
WORKDIR /app
20+
ENV NODE_ENV=production
21+
COPY --from=build /app/dist/apps/mcp-host ./dist
22+
COPY --from=build /app/apps/mcp-host/server.mjs ./server.mjs
23+
EXPOSE 8080 8081
24+
CMD ["node", "server.mjs"]

apps/mcp-host/eslint.config.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import baseConfig from '../../eslint.config.mjs';
2+
3+
export default [...baseConfig];

0 commit comments

Comments
 (0)