Monorepo patterns

A FrontMCP workspace has three kinds of project. Apps in apps/ hold capabilities: tools, resources, prompts. Libraries in libs/ hold code several apps share. Server shells in servers/ decide which apps are served together, with which options, for which platform. Nx sees the imports between them, so it knows what to rebuild and retest when something changes. In 1.9.2 the generators make all three, each runs with nx dev and type-checks with nx typecheck as generated, and the CLI bundles code an app imports from outside its folder, a library through its alias or a shell's apps by relative path, with nothing to set up first.

help-desk-platform/
  apps/tickets/        @App: the ticket tools        nx dev tickets
  apps/billing/        @App: invoices                nx dev billing
  libs/ticket-model/   shared types and helpers      imported by apps
  servers/gateway/     @FrontMcp: tickets + billing  nx dev server-gateway

Reference

The three layers

LayerContainsImportsMade by
apps/<name>An @App and its tools, resources, prompts, skills, jobs; a main.ts that serves it alone.Libraries.nx g @frontmcp/nx:app
libs/<name>Plain TypeScript: types, clients, helpers, shared tools or plugins.Other libraries.nx g @frontmcp/nx:lib
servers/<name>A @FrontMcp server listing apps, with the options and the deployment files of one environment.Apps and libraries.nx g @frontmcp/nx:server

Nx finds these dependencies in the imports, and shows them in the project graph:

npx nx graph --file=graph.json
server-gateway -> demo, tickets
tickets -> ticket-model

Because nx.json makes build depend on ^build, nx build server-gateway builds demo and tickets first (Successfully ran target build for project server-gateway and 2 tasks it depends on). A library has no build of its own: each bundle that imports it compiles it in.

What each pattern needs

Patternnx devnx buildnx typecheck
An app that imports nothing outside its folderWorks.Works.Works.
A server shell that imports appsWorks.Works: the apps build first, then the shell, into one bundle.Works, checking the apps it imports too.
An app that imports a library through a tsconfig.base.json aliasWorks.Works.Works.

The executors run the CLI in the project's folder (Executors). frontmcp dev runs the entry with tsx, which reads path aliases from the project's own tsconfig.json, and that file extends tsconfig.base.json, where the lib generator put the alias. frontmcp build compiles the project with that tsconfig.json, mirrors the folders it imports from in the output, bundles all of it, and removes the mirrored folders afterwards. A bundle holds the libraries and apps it imports, so the built server runs without them, and dist/node/ has only the bundle, the runner, the manifest and the installer. Before 1.8.7 all of this failed: dev couldn't find the alias without a root tsconfig.json, and builds stopped with TS6059 … is not under 'rootDir' unless the entry sat at the workspace root. In 1.8.7 an app that imported a library still needed a root tsconfig.json to build, and nx typecheck failed in every project.

Caveats

  • In a workspace made with 1.8.7, nx.json lists the @nx/js/typescript plugin, which gives each library an inferred build that needs a root tsconfig.json. Remove it: Upgrading a 1.8.7 workspace.
  • The lib generator's default alias takes the npm scope of the workspace's root package.json, and is the bare name when it has none, as in a workspace from frontmcp create --nx (ticket-model). Pass --importPath to choose it, as below. Before 1.9.2 the default was @frontmcp/<name>, under FrontMCP's own npm scope.

Usage

Composing apps into one server

Apps stay independent, each with its own main.ts for nx dev <app>, and a server shell lists several. When two apps have a tool with the same name, FrontMCP keeps both and prefixes each with its app's id, as <id>:<name> (Handling name clashes). The app generator gives every app a hello tool, so a shell of two generated apps shows it at once:

Open
import { FrontMcp } from "@frontmcp/sdk";
import { DemoApp } from "./demo.app";
import { TicketsApp } from "./tickets.app";

// servers/gateway/src/main.ts
@FrontMcp({
  info: { name: "Gateway Server", version: "0.1.0" },
  apps: [DemoApp, TicketsApp],
})
export default class Server {}

Starting FrontMCP in your browser…

FrontMCP starts when this example comes into view.

In the workspace, generate the shell, and run it with its dev target:

npx nx g @frontmcp/nx:server gateway --apps demo,tickets --skills none
npx nx dev server-gateway --port=4110
Running: frontmcp dev --entry /Users/you/help-desk-platform/servers/gateway/src/main.ts --port 4110 (in /Users/you/help-desk-platform/servers/gateway)
…
MCP HTTP (Express) on 127.0.0.1:4110
[10:01:12 AM] Found 0 errors. Watching for file changes.

A client connected to http://localhost:4110/ lists demo:hello and tickets:hello, as in the Playground. Give tools names that don't clash when you can: the prefix is only added when two apps collide, so adding an app can rename another app's tools.

Building a composed server

The shell's own main.ts is the entry, and it imports the apps by relative path. nx build server-gateway bundles all of it:

$ npx nx build server-gateway
 NX   Running target build for project server-gateway and 2 tasks it depends on:
Running: frontmcp build --entry …/apps/demo/src/main.ts --out-dir …/apps/demo/dist (in …/apps/demo)
Running: frontmcp build --entry …/apps/tickets/src/main.ts --out-dir …/apps/tickets/dist (in …/apps/tickets)
Running: frontmcp build --target node --entry …/servers/gateway/src/main.ts --out-dir …/servers/gateway/dist (in …/servers/gateway)
[build:exec] bundle created: dist/node/server-gateway.bundle.js (26.8 KB)

 NX   Successfully ran target build for project server-gateway and 2 tasks it depends on

PORT=8080 servers/gateway/dist/node/server-gateway serves both apps, with demo:hello and tickets:hello answering. Here tickets imports the library of the next section, which is compiled into both bundles. Before 1.8.7 this needed an entry at the workspace root that re-exported the shell's server.

Sharing code in a library

npx nx g @frontmcp/nx:lib ticket-model --importPath @help-desk/ticket-model

It writes libs/ticket-model/ and adds the alias to tsconfig.base.json:

tsconfig.base.json
{
  "compilerOptions": {
    "paths": { "@help-desk/ticket-model": ["./libs/ticket-model/src/index.ts"] }
  }
}

(Only paths is shown; the rest of the file stays.) Put code in it, and export it from index.ts, which the generator wrote to export its sample class by name:

libs/ticket-model/src/ticket-model.ts
export const PRIORITIES = ['low', 'normal', 'urgent'] as const;
export type Priority = (typeof PRIORITIES)[number];

export function formatTicketId(n: number): string {
  return `T-${String(n).padStart(4, '0')}`;
}
libs/ticket-model/src/index.ts
export * from './ticket-model';

Import it from an app:

apps/tickets/src/tools/hello.tool.ts
import { Tool, ToolContext, z } from '@frontmcp/sdk';
import { formatTicketId } from '@help-desk/ticket-model';

@Tool({
  name: 'hello',
  description: 'Say hello to someone',
  inputSchema: { name: z.string().describe('Name to greet') },
  outputSchema: { message: z.string() },
})
export default class HelloTool extends ToolContext {
  async execute(input: { name: string }) {
    return { message: `Hello, ${input.name}! Your ticket is ${formatTicketId(7)}.` };
  }
}

nx dev tickets serves it as it is: the CLI runs in apps/tickets/, where tsconfig.json extends the base config that has the alias, and frontmcp dev's type checker reports Found 0 errors. Calling the tool returns {"message":"Hello, Ada! Your ticket is T-0007."}, and the graph shows tickets -> ticket-model. A spec in the app can import the alias too: the generated jest.config.cjs maps every alias in paths. Update the generated hello.tool.spec.ts to the new message, or nx test tickets fails on it.

nx build tickets and nx typecheck tickets work too:

$ npx nx build tickets
> nx run tickets:build
…
[build:exec] bundle created: dist/node/tickets.bundle.js (25.1 KB)

 NX   Successfully ran target build for project tickets

The library is compiled into the bundle, so the built server runs without it: PORT=8080 apps/tickets/dist/node/tickets. In 1.8.7 this build stopped first with [@nx/js:typescript-sync]: Missing root "tsconfig.json", from a build that Nx's TypeScript plugin gave the library, until you added a root tsconfig.json (Troubleshooting).

One server per environment

Each server shell is its own @FrontMcp server, so shells can serve different apps, with different options, from the same code:

servers/staging/src/main.ts
import 'reflect-metadata';
import { FrontMcp } from '@frontmcp/sdk';
import { DemoApp } from '../../../apps/demo/src/demo.app';
import { TicketsApp } from '../../../apps/tickets/src/tickets.app';

@FrontMcp({
  info: { name: 'Help Desk (staging)', version: '0.1.0' },
  apps: [DemoApp, TicketsApp], // every app, to try them out
})
export default class Server {}
servers/production/src/main.ts
import 'reflect-metadata';
import { FrontMcp } from '@frontmcp/sdk';
import { TicketsApp } from '../../../apps/tickets/src/tickets.app';

@FrontMcp({
  info: { name: 'Help Desk', version: '1.0.0' },
  apps: [TicketsApp], // only what's released
  // auth, redis, http… for this environment: see @FrontMcp
})
export default class Server {}

@FrontMcp has the options, like auth, redis and http, that differ between environments. Secrets and addresses belong in each environment's variables, not in the shell: Configuration files. Each shell the generator makes has a dev target, so two run side by side with nx dev server-staging --port=4110 and nx dev server-production --port=4111.

Tagging projects and finding what changed

--tags on the generators writes tags into project.json (the default is scope:apps for apps, scope:servers for shells and scope:libs for libraries). Nx selects projects by tag:

npx nx g @frontmcp/nx:app billing --tags scope:billing,type:app
npx nx show projects --projects "tag:scope:billing"
npx nx show projects --projects "tag:scope:servers"
["billing"]
["server-gateway"]

After a change to apps/tickets/, Nx lists the projects it affects, the app and every shell that imports it, and after a change to a library, the library as well:

npx nx show projects --affected --base=HEAD
["tickets","server-gateway"]
["ticket-model","tickets","server-gateway"]

npx nx affected -t build builds only those. Nx can also enforce which tags may import which, with ESLint; the workspace frontmcp create --nx makes has no ESLint configuration, so that's yours to add.

Moving a standalone project into a workspace

  1. Create the workspace, next to the project: npx frontmcp create help-desk-platform --nx (Nx plugin).

  2. Generate an app for it: npx nx g @frontmcp/nx:app help-desk.

  3. Copy the code: cp ../help-desk/src/tools/*.ts apps/help-desk/src/tools/, and the same for resources and prompts. If you don't want the generated hello tool, delete hello.tool.ts and its test, hello.tool.spec.ts.

  4. List them in the app, which the generator named HelpDeskApp:

    apps/help-desk/src/help-desk.app.ts
    import { App } from '@frontmcp/sdk';
    import AddTool from './tools/add.tool';
    
    @App({
      id: 'help-desk',
      name: 'HelpDesk',
      tools: [AddTool],
    })
    export class HelpDeskApp {}

    The generated main.ts serves HelpDeskApp. If your old main.ts set options (auth, http, redis…), copy them into it.

  5. Copy the tests to apps/help-desk/e2e/, with a port of their own in test.use. The test target the generator wrote runs them: it finds jest.config.cjs in the app, which loads the matchers.

  6. Check it:

    npx nx dev help-desk
    npx nx test help-desk
    npx nx build help-desk
    Tests:       4 passed, 4 total
     NX   Successfully ran target test for project help-desk
    [build:exec] bundle created: dist/node/help-desk.bundle.js (24.4 KB)
     NX   Successfully ran target build for project help-desk
  7. Move code other apps will need into libs/ as above.

frontmcp.config.ts comes along: copy it into apps/help-desk/. The executors run the CLI there, so dev, build, test and inspector read it, and its name, version, transport and env apply to that app. A .env in the app's folder is read by dev too.


Troubleshooting

The workspace is probably out of sync because a sync generator failed to run

With [@nx/js:typescript-sync]: Missing root "tsconfig.json", from nx build of an app that imports a library, in a workspace made with 1.8.7. Its nx.json lists the @nx/js/typescript plugin, which gives each library an inferred build that needs a root tsconfig.json. Remove the plugin from nx.json, as 1.9.2 workspaces don't have it (Upgrading a 1.8.7 workspace). Or keep it and add a root tsconfig.json, which can be nearly empty:

tsconfig.json
{
  "extends": "./tsconfig.base.json",
  "files": [],
  "references": []
}

Cannot find module '@help-desk/ticket-model'

The alias isn't in tsconfig.base.json's paths, or the app's tsconfig.json doesn't extend that file. Run the lib generator in this workspace, or add the alias.

error TS5069 from nx typecheck

A project made before 1.9, with no typecheck target of its own, in a workspace with Nx's TypeScript plugin, whose inferred one fails. Add the typecheck target a 1.9.2 project has (Nx plugin), or use npx tsc -p apps/<app>/tsconfig.lib.json --noEmit.

A tool's name has its app's id in front, like tickets:hello

Two apps on the server have a tool with the same name. Rename one, or call the prefixed name. See Handling name clashes.

nx affected doesn't list a server shell

The shell doesn't import the changed app. Check the imports in servers/<name>/src/main.ts: the generator writes relative paths to each app's src/<app>.app.

Could not resolve "…/dist/node/main.js"

tsc emitted no JavaScript, because the workspace's tsconfig.base.json sets emitDeclarationOnly and the app's tsconfig.json, from before 1.9, doesn't turn it off. See Nx plugin.