Back to skills
SkillHub ClubRun DevOpsFull StackBackendDesigner

interop-forge

Integration architect for multi-app monorepos — shared contracts, API-first design with OpenAPI, cross-app auth, auto-generated SDKs, and full MCP server scaffolding per app

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,036
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
C67.6

Install command

npx @skill-hub/cli install openclaw-skills-interop-forge

Repository

openclaw/skills

Skill path: skills/guifav/interop-forge

Integration architect for multi-app monorepos — shared contracts, API-first design with OpenAPI, cross-app auth, auto-generated SDKs, and full MCP server scaffolding per app

Open repository

Best for

Primary workflow: Run DevOps.

Technical facets: Full Stack, Backend, Designer, Security, Integration.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install interop-forge into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding interop-forge to shared team environments
  • Use interop-forge for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: interop-forge
description: Integration architect for multi-app monorepos — shared contracts, API-first design with OpenAPI, cross-app auth, auto-generated SDKs, and full MCP server scaffolding per app
user-invocable: true
---

# Interop Forge

You are a senior integration architect responsible for ensuring that multiple applications within a monorepo can interoperate seamlessly now and integrate fully in the future. You design shared contracts (types, schemas, validators), enforce API-first development with OpenAPI specifications, configure cross-app authentication, generate typed SDKs from specs, and scaffold full MCP servers so each app can be orchestrated by AI agents. This skill is stack-agnostic — it detects whether the project uses Vercel/Supabase, GCP, or another stack and adapts accordingly. This skill creates TypeScript packages, OpenAPI specs, MCP server files, and configuration files. It never reads or modifies `.env`, `.env.local`, or credential files directly.

**Credential scope:** `OPENROUTER_API_KEY` is optionally used in generated MCP server code for apps that expose LLM-powered tools. `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `GCP_PROJECT_ID`, and `GOOGLE_APPLICATION_CREDENTIALS` are referenced in generated inter-app SDK code and MCP server implementations when the target app uses those services. All env vars are accessed via `process.env` in generated code only — this skill never makes direct API calls itself.

## Planning Protocol (MANDATORY — execute before ANY action)

Before creating any shared package, spec, or MCP server, you MUST complete this planning phase:

1. **Understand the integration goal.** Determine: (a) which apps need to interoperate, (b) what data or functionality is shared, (c) whether integration is real-time or async, (d) the direction of data flow (bidirectional, producer/consumer, hub/spoke).

2. **Survey the monorepo.** Check: (a) monorepo tool (turborepo, nx, pnpm workspaces, yarn workspaces), (b) existing shared packages in `packages/`, (c) existing OpenAPI specs, (d) auth strategy per app, (e) database topology (shared instance vs separate), (f) existing MCP servers. Read `turbo.json`, `pnpm-workspace.yaml`, `nx.json`, or `package.json` workspaces config.

3. **Map the app landscape.** For each app, document: (a) name, (b) stack (Next.js, Nuxt, SvelteKit, etc.), (c) database (Supabase, Firestore, Cloud SQL, none), (d) auth provider (Firebase, Supabase Auth, Identity Platform), (e) existing API routes, (f) existing MCP server (if any).

4. **Identify shared surfaces.** Classify what should be shared: (a) types and interfaces, (b) validation schemas (Zod), (c) API contracts (OpenAPI), (d) auth tokens and user identity, (e) event schemas, (f) utility functions.

5. **Design the integration architecture.** Choose patterns: (a) shared contracts package, (b) API-first with generated SDK, (c) shared auth with JWT forwarding, (d) database sharing strategy, (e) MCP server topology.

6. **Build the execution plan.** List: (a) packages to create/modify, (b) specs to write, (c) SDKs to generate, (d) MCP servers to scaffold, (e) turbo pipeline changes.

7. **Execute incrementally.** Create packages one at a time. Verify each builds before proceeding.

8. **Summarize.** Report: packages created, specs generated, SDKs built, MCP servers scaffolded, and any manual steps remaining.

Do NOT skip this protocol. Rushing integration architecture leads to circular dependencies, type mismatches, and auth holes between apps.

---

## Part 1 — Monorepo Structure

### Expected Directory Layout

```
my-monorepo/
├── apps/
│   ├── app-one/          # Next.js, Nuxt, SvelteKit, etc.
│   ├── app-two/
│   └── app-three/
├── packages/
│   ├── contracts/         # Shared types, Zod schemas, constants
│   ├── api-specs/         # OpenAPI specifications per app
│   ├── sdk/               # Auto-generated typed clients
│   ├── auth/              # Shared auth utilities
│   ├── mcp-core/          # Shared MCP server utilities
│   └── eslint-config/     # Shared ESLint config (optional)
├── mcp-servers/
│   ├── app-one-mcp/       # MCP server exposing app-one's capabilities
│   ├── app-two-mcp/
│   └── app-three-mcp/
├── turbo.json
├── pnpm-workspace.yaml
└── package.json
```

### Monorepo Tool Detection and Setup

```bash
# Detect monorepo tool
if [ -f "turbo.json" ]; then
  MONOREPO="turborepo"
elif [ -f "nx.json" ]; then
  MONOREPO="nx"
elif grep -q '"workspaces"' package.json 2>/dev/null; then
  MONOREPO="pnpm-workspaces"  # or yarn
fi
```

If no monorepo tool exists, set up Turborepo (recommended default):

```yaml
# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"
  - "mcp-servers/*"
```

```json
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "typecheck": {
      "dependsOn": ["^build"]
    },
    "generate:sdk": {
      "dependsOn": ["^build"],
      "outputs": ["packages/sdk/src/generated/**"]
    },
    "mcp:dev": {
      "cache": false,
      "persistent": true,
      "dependsOn": ["^build"]
    }
  }
}
```

---

## Part 2 — Shared Contracts Package

The contracts package is the **single source of truth** for all shared types, validation schemas, and constants across apps.

### Package Setup

```json
// packages/contracts/package.json
{
  "name": "@repo/contracts",
  "version": "0.1.0",
  "private": true,
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    },
    "./schemas": {
      "import": "./dist/schemas/index.js",
      "types": "./dist/schemas/index.d.ts"
    },
    "./events": {
      "import": "./dist/events/index.js",
      "types": "./dist/events/index.d.ts"
    }
  },
  "scripts": {
    "build": "tsup src/index.ts src/schemas/index.ts src/events/index.ts --format esm --dts",
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "tsup": "^8.0.0",
    "typescript": "^5.4.0"
  },
  "dependencies": {
    "zod": "^3.23.0"
  }
}
```

### Shared Types

```typescript
// packages/contracts/src/index.ts
export * from "./types";
export * from "./constants";
export * from "./schemas";
export * from "./events";
```

```typescript
// packages/contracts/src/types/user.ts
export interface SharedUser {
  id: string;
  email: string;
  displayName: string;
  avatarUrl?: string;
  provider: "firebase" | "supabase" | "identity-platform";
  metadata: {
    createdAt: string;
    lastLoginAt: string;
    source: string; // which app created this user
  };
}

export interface CrossAppToken {
  sub: string;          // user ID
  email: string;
  iss: string;          // issuing app name
  aud: string[];        // target app names
  iat: number;
  exp: number;
  permissions: string[];
}
```

```typescript
// packages/contracts/src/types/api.ts
export interface ApiResponse<T> {
  data: T;
  meta?: {
    page?: number;
    perPage?: number;
    total?: number;
  };
}

export interface ApiError {
  error: {
    code: string;
    message: string;
    details?: Record<string, unknown>;
  };
}

export type ApiResult<T> = ApiResponse<T> | ApiError;

// Standard pagination params
export interface PaginationParams {
  page?: number;
  perPage?: number;
  sortBy?: string;
  sortOrder?: "asc" | "desc";
}

// Standard filter params
export interface FilterParams {
  search?: string;
  startDate?: string;
  endDate?: string;
  status?: string;
}
```

### Shared Zod Schemas

```typescript
// packages/contracts/src/schemas/user.schema.ts
import { z } from "zod";

export const SharedUserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  displayName: z.string().min(1).max(100),
  avatarUrl: z.string().url().optional(),
  provider: z.enum(["firebase", "supabase", "identity-platform"]),
  metadata: z.object({
    createdAt: z.string().datetime(),
    lastLoginAt: z.string().datetime(),
    source: z.string(),
  }),
});

export type SharedUser = z.infer<typeof SharedUserSchema>;
```

```typescript
// packages/contracts/src/schemas/pagination.schema.ts
import { z } from "zod";

export const PaginationSchema = z.object({
  page: z.coerce.number().int().positive().default(1),
  perPage: z.coerce.number().int().positive().max(100).default(20),
  sortBy: z.string().optional(),
  sortOrder: z.enum(["asc", "desc"]).default("desc"),
});

export const FilterSchema = z.object({
  search: z.string().optional(),
  startDate: z.string().datetime().optional(),
  endDate: z.string().datetime().optional(),
  status: z.string().optional(),
});
```

### Shared Event Schemas (for inter-app communication)

```typescript
// packages/contracts/src/events/index.ts
import { z } from "zod";

export const BaseEventSchema = z.object({
  id: z.string().uuid(),
  type: z.string(),
  source: z.string(),       // app name that emitted the event
  timestamp: z.string().datetime(),
  version: z.literal("1.0"),
  payload: z.record(z.unknown()),
});

export type BaseEvent = z.infer<typeof BaseEventSchema>;

// Example domain events
export const UserCreatedEventSchema = BaseEventSchema.extend({
  type: z.literal("user.created"),
  payload: z.object({
    userId: z.string(),
    email: z.string().email(),
    provider: z.string(),
  }),
});

export const EntityUpdatedEventSchema = BaseEventSchema.extend({
  type: z.literal("entity.updated"),
  payload: z.object({
    entityId: z.string(),
    changes: z.record(z.unknown()),
    updatedBy: z.string(),
  }),
});

// Event type registry — add new events here
export const EventSchemaRegistry = {
  "user.created": UserCreatedEventSchema,
  "entity.updated": EntityUpdatedEventSchema,
} as const;

export type EventType = keyof typeof EventSchemaRegistry;
```

### How Apps Consume Contracts

In any app's `package.json`:

```json
{
  "dependencies": {
    "@repo/contracts": "workspace:*"
  }
}
```

Usage:

```typescript
import { SharedUserSchema, type SharedUser } from "@repo/contracts/schemas";
import { PaginationSchema } from "@repo/contracts/schemas";
import type { ApiResponse, ApiError } from "@repo/contracts";
```

---

## Part 3 — API-First Design with OpenAPI

Every app MUST define its public API as an OpenAPI 3.1 spec BEFORE implementing the endpoints. The spec lives in `packages/api-specs/`.

### Spec Structure

```yaml
# packages/api-specs/app-one.openapi.yaml
openapi: "3.1.0"
info:
  title: App One API
  version: "1.0.0"
  description: Public API for App One
  contact:
    name: Team
servers:
  - url: https://app-one.vercel.app/api
    description: Production
  - url: http://localhost:3001/api
    description: Local development

paths:
  /entities:
    get:
      operationId: listEntities
      summary: List all entities for the authenticated user
      tags: [Entities]
      security:
        - BearerAuth: []
      parameters:
        - $ref: "#/components/parameters/Page"
        - $ref: "#/components/parameters/PerPage"
        - $ref: "#/components/parameters/Search"
      responses:
        "200":
          description: Paginated list of entities
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EntityListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: createEntity
      summary: Create a new entity
      tags: [Entities]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEntityInput"
      responses:
        "201":
          description: Entity created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EntityResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /entities/{id}:
    get:
      operationId: getEntity
      summary: Get a single entity
      tags: [Entities]
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Entity details
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EntityResponse"
        "404":
          $ref: "#/components/responses/NotFound"

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

  parameters:
    Page:
      name: page
      in: query
      schema:
        type: integer
        default: 1
    PerPage:
      name: perPage
      in: query
      schema:
        type: integer
        default: 20
        maximum: 100
    Search:
      name: search
      in: query
      schema:
        type: string

  schemas:
    Entity:
      type: object
      required: [id, name, createdAt]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
        status:
          type: string
          enum: [active, archived]
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CreateEntityInput:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
          maxLength: 2000

    EntityResponse:
      type: object
      properties:
        data:
          $ref: "#/components/schemas/Entity"

    EntityListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/Entity"
        meta:
          $ref: "#/components/schemas/PaginationMeta"

    PaginationMeta:
      type: object
      properties:
        page:
          type: integer
        perPage:
          type: integer
        total:
          type: integer

  responses:
    Unauthorized:
      description: Authentication required
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                properties:
                  code:
                    type: string
                    example: UNAUTHORIZED
                  message:
                    type: string
                    example: Authentication required

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                properties:
                  code:
                    type: string
                    example: NOT_FOUND
                  message:
                    type: string

    ValidationError:
      description: Input validation failed
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: object
                properties:
                  code:
                    type: string
                    example: VALIDATION_ERROR
                  message:
                    type: string
                  details:
                    type: object
```

### Spec Validation

Before generating an SDK, always validate the spec:

```bash
npx @redocly/cli lint packages/api-specs/app-one.openapi.yaml
```

---

## Part 4 — Auto-Generated Typed SDKs

Generate a typed TypeScript client from each OpenAPI spec so apps can call each other with full type safety.

### SDK Package Setup

```json
// packages/sdk/package.json
{
  "name": "@repo/sdk",
  "version": "0.1.0",
  "private": true,
  "exports": {
    "./app-one": {
      "import": "./src/generated/app-one/index.ts",
      "types": "./src/generated/app-one/index.ts"
    },
    "./app-two": {
      "import": "./src/generated/app-two/index.ts",
      "types": "./src/generated/app-two/index.ts"
    }
  },
  "scripts": {
    "generate": "pnpm generate:app-one && pnpm generate:app-two",
    "generate:app-one": "openapi-typescript ../api-specs/app-one.openapi.yaml -o src/generated/app-one/schema.d.ts",
    "generate:app-two": "openapi-typescript ../api-specs/app-two.openapi.yaml -o src/generated/app-two/schema.d.ts"
  },
  "devDependencies": {
    "openapi-typescript": "^7.0.0",
    "openapi-fetch": "^0.10.0"
  }
}
```

### SDK Client Wrapper

```typescript
// packages/sdk/src/generated/app-one/index.ts
import createClient from "openapi-fetch";
import type { paths } from "./schema";

export function createAppOneClient(options: {
  baseUrl: string;
  token: string;
}) {
  return createClient<paths>({
    baseUrl: options.baseUrl,
    headers: {
      Authorization: `Bearer ${options.token}`,
    },
  });
}

// Re-export types for convenience
export type { paths } from "./schema";
```

### Usage in Another App

```typescript
// apps/app-two/src/lib/app-one-client.ts
import { createAppOneClient } from "@repo/sdk/app-one";

const appOneClient = createAppOneClient({
  baseUrl: process.env.APP_ONE_API_URL!,
  token: process.env.CROSS_APP_TOKEN!,
});

// Fully typed — IDE autocomplete works
const { data, error } = await appOneClient.GET("/entities", {
  params: { query: { page: 1, perPage: 10 } },
});

if (data) {
  // data.data is Entity[], data.meta is PaginationMeta — all typed
}
```

### SDK Generation Pipeline

Add to turbo.json:

```json
{
  "generate:sdk": {
    "dependsOn": ["^build"],
    "inputs": ["packages/api-specs/**/*.yaml"],
    "outputs": ["packages/sdk/src/generated/**"]
  }
}
```

Run: `pnpm turbo generate:sdk`

---

## Part 5 — Cross-App Authentication

### Strategy: JWT Forwarding with Shared Verification

All apps share the same auth provider (Firebase or Supabase Auth). When app-two calls app-one's API, it forwards the user's JWT. App-one verifies the token using the same provider.

### Shared Auth Package

```json
// packages/auth/package.json
{
  "name": "@repo/auth",
  "version": "0.1.0",
  "private": true,
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "scripts": {
    "build": "tsup src/index.ts --format esm --dts"
  }
}
```

```typescript
// packages/auth/src/index.ts
export { verifyToken, type TokenPayload } from "./verify";
export { createCrossAppToken } from "./cross-app";
export { authMiddleware } from "./middleware";
```

### Token Verification (stack-agnostic)

```typescript
// packages/auth/src/verify.ts
import type { CrossAppToken } from "@repo/contracts";

export interface TokenPayload {
  sub: string;
  email: string;
  iss: string;
  permissions: string[];
}

type AuthProvider = "firebase" | "supabase";

export async function verifyToken(
  token: string,
  provider: AuthProvider
): Promise<TokenPayload> {
  if (provider === "firebase") {
    // Dynamic import to avoid bundling both SDKs
    const { getAuth } = await import("firebase-admin/auth");
    const decoded = await getAuth().verifyIdToken(token);
    return {
      sub: decoded.uid,
      email: decoded.email ?? "",
      iss: "firebase",
      permissions: (decoded.permissions as string[]) ?? [],
    };
  }

  if (provider === "supabase") {
    const { createClient } = await import("@supabase/supabase-js");
    const supabase = createClient(
      process.env.SUPABASE_URL!,
      process.env.SUPABASE_ANON_KEY!
    );
    const { data, error } = await supabase.auth.getUser(token);
    if (error || !data.user) throw new Error("Invalid token");
    return {
      sub: data.user.id,
      email: data.user.email ?? "",
      iss: "supabase",
      permissions: (data.user.app_metadata?.permissions as string[]) ?? [],
    };
  }

  throw new Error(`Unsupported auth provider: ${provider}`);
}
```

### Cross-App Token Generation

```typescript
// packages/auth/src/cross-app.ts
import { SignJWT, jwtVerify } from "jose";

const SECRET = new TextEncoder().encode(
  process.env.CROSS_APP_SECRET! // Shared secret between apps (32+ chars)
);

export async function createCrossAppToken(payload: {
  sub: string;
  email: string;
  sourceApp: string;
  targetApps: string[];
  permissions: string[];
}): Promise<string> {
  return new SignJWT({
    sub: payload.sub,
    email: payload.email,
    iss: payload.sourceApp,
    aud: payload.targetApps,
    permissions: payload.permissions,
  })
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("5m") // Short-lived for security
    .sign(SECRET);
}

export async function verifyCrossAppToken(
  token: string,
  expectedAudience: string
) {
  const { payload } = await jwtVerify(token, SECRET, {
    audience: expectedAudience,
  });
  return payload;
}
```

### Auth Middleware (reusable across apps)

```typescript
// packages/auth/src/middleware.ts
import { verifyToken, type TokenPayload } from "./verify";
import { verifyCrossAppToken } from "./cross-app";
import type { NextRequest } from "next/server";

type AuthProvider = "firebase" | "supabase";

export function authMiddleware(options: {
  provider: AuthProvider;
  appName: string;
  allowCrossApp?: boolean;
}) {
  return async function (request: NextRequest): Promise<TokenPayload | null> {
    const authHeader = request.headers.get("Authorization");
    if (!authHeader?.startsWith("Bearer ")) return null;
    const token = authHeader.slice(7);

    // Try standard auth first
    try {
      return await verifyToken(token, options.provider);
    } catch {
      // If standard auth fails and cross-app is enabled, try cross-app token
      if (options.allowCrossApp) {
        try {
          const payload = await verifyCrossAppToken(token, options.appName);
          return {
            sub: payload.sub as string,
            email: (payload.email as string) ?? "",
            iss: payload.iss ?? "unknown",
            permissions: (payload.permissions as string[]) ?? [],
          };
        } catch {
          return null;
        }
      }
      return null;
    }
  };
}
```

---

## Part 6 — Database Sharing Patterns

### Pattern A: Shared Database Instance (Same Supabase/Firestore)

When multiple apps share the same database, use schema-based isolation:

```sql
-- Supabase: each app gets its own schema
CREATE SCHEMA IF NOT EXISTS app_one;
CREATE SCHEMA IF NOT EXISTS app_two;
CREATE SCHEMA IF NOT EXISTS shared;  -- cross-app tables live here

-- Shared users table (single source of truth)
CREATE TABLE shared.users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT UNIQUE NOT NULL,
  display_name TEXT NOT NULL,
  avatar_url TEXT,
  provider TEXT NOT NULL,
  source_app TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

-- App-specific tables use their own schema
CREATE TABLE app_one.entities (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID REFERENCES shared.users(id),
  name TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now()
);

-- RLS: each app can only see data within its schema + shared
ALTER TABLE app_one.entities ENABLE ROW LEVEL SECURITY;
CREATE POLICY "users see own entities"
  ON app_one.entities FOR ALL
  USING (user_id = auth.uid());
```

For **Firestore**, use top-level collection prefixes:

```
/shared/users/{userId}
/app-one/entities/{entityId}
/app-two/items/{itemId}
```

### Pattern B: Separate Database Instances

When apps have their own database, communication happens via API calls using the generated SDK:

```typescript
// app-two needs data from app-one
import { createAppOneClient } from "@repo/sdk/app-one";
import { createCrossAppToken } from "@repo/auth";

async function getDataFromAppOne(userId: string) {
  const token = await createCrossAppToken({
    sub: userId,
    email: "[email protected]",
    sourceApp: "app-two",
    targetApps: ["app-one"],
    permissions: ["entities:read"],
  });

  const client = createAppOneClient({
    baseUrl: process.env.APP_ONE_API_URL!,
    token,
  });

  return client.GET("/entities");
}
```

### Decision Rule

| Scenario | Use shared DB | Use separate DBs + API |
|----------|:---:|:---:|
| Apps share user accounts | yes | - |
| Apps share business entities | yes | - |
| Apps are independently deployable | - | yes |
| Apps may be split into separate repos later | - | yes |
| Real-time sync needed between apps | yes | - |
| Apps have different scaling requirements | - | yes |
| Regulatory isolation required | - | yes |

---

## Part 7 — MCP Server Scaffolding

Each app exposes a full MCP server that allows AI agents to interact with its capabilities.

### MCP Server Structure

```
mcp-servers/
├── app-one-mcp/
│   ├── package.json
│   ├── tsconfig.json
│   ├── src/
│   │   ├── index.ts          # Server entry point
│   │   ├── tools/             # One file per tool
│   │   │   ├── list-entities.ts
│   │   │   ├── create-entity.ts
│   │   │   ├── get-entity.ts
│   │   │   └── search-entities.ts
│   │   ├── resources/         # Exposed data resources
│   │   │   └── entity-schema.ts
│   │   └── auth.ts            # Auth handling for MCP
│   └── mcp.json               # MCP manifest
```

### MCP Server Entry Point Template

```typescript
// mcp-servers/app-one-mcp/src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { listEntitiesTool } from "./tools/list-entities.js";
import { createEntityTool } from "./tools/create-entity.js";
import { getEntityTool } from "./tools/get-entity.js";
import { searchEntitiesTool } from "./tools/search-entities.js";

const server = new McpServer({
  name: "app-one-mcp",
  version: "1.0.0",
});

// Register tools
listEntitiesTool(server);
createEntityTool(server);
getEntityTool(server);
searchEntitiesTool(server);

// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
```

### MCP Tool Template

```typescript
// mcp-servers/app-one-mcp/src/tools/list-entities.ts
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { createAppOneClient } from "@repo/sdk/app-one";

const ParamsSchema = z.object({
  page: z.number().int().positive().default(1).describe("Page number"),
  perPage: z.number().int().positive().max(100).default(20).describe("Items per page"),
  search: z.string().optional().describe("Search term to filter entities"),
});

export function listEntitiesTool(server: McpServer) {
  server.tool(
    "list-entities",
    "List entities from App One with pagination and optional search",
    ParamsSchema.shape,
    async (params) => {
      const validated = ParamsSchema.parse(params);

      const client = createAppOneClient({
        baseUrl: process.env.APP_ONE_API_URL!,
        token: process.env.APP_ONE_SERVICE_TOKEN!,
      });

      const { data, error } = await client.GET("/entities", {
        params: { query: validated },
      });

      if (error) {
        return {
          content: [{ type: "text", text: `Error: ${JSON.stringify(error)}` }],
          isError: true,
        };
      }

      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(data, null, 2),
          },
        ],
      };
    }
  );
}
```

### MCP Tool for Cross-App Operations

```typescript
// mcp-servers/app-one-mcp/src/tools/create-entity.ts
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { createAppOneClient } from "@repo/sdk/app-one";
import { CreateEntityInputSchema } from "@repo/contracts/schemas";

export function createEntityTool(server: McpServer) {
  server.tool(
    "create-entity",
    "Create a new entity in App One",
    {
      name: z.string().min(1).max(200).describe("Entity name"),
      description: z.string().max(2000).optional().describe("Entity description"),
    },
    async (params) => {
      // Validate against shared contract
      const validated = CreateEntityInputSchema.parse(params);

      const client = createAppOneClient({
        baseUrl: process.env.APP_ONE_API_URL!,
        token: process.env.APP_ONE_SERVICE_TOKEN!,
      });

      const { data, error } = await client.POST("/entities", {
        body: validated,
      });

      if (error) {
        return {
          content: [{ type: "text", text: `Error creating entity: ${JSON.stringify(error)}` }],
          isError: true,
        };
      }

      return {
        content: [
          {
            type: "text",
            text: `Entity created successfully:\n${JSON.stringify(data, null, 2)}`,
          },
        ],
      };
    }
  );
}
```

### MCP Manifest

```json
// mcp-servers/app-one-mcp/mcp.json
{
  "name": "app-one-mcp",
  "version": "1.0.0",
  "description": "MCP server for App One — manage entities, search, and CRUD operations",
  "tools": [
    {
      "name": "list-entities",
      "description": "List entities with pagination and search"
    },
    {
      "name": "create-entity",
      "description": "Create a new entity"
    },
    {
      "name": "get-entity",
      "description": "Get entity details by ID"
    },
    {
      "name": "search-entities",
      "description": "Full-text search across entities"
    }
  ],
  "env": {
    "APP_ONE_API_URL": {
      "description": "Base URL of App One's API",
      "required": true
    },
    "APP_ONE_SERVICE_TOKEN": {
      "description": "Service token for authenticating MCP server to App One",
      "required": true
    }
  }
}
```

### MCP Server Package.json

```json
// mcp-servers/app-one-mcp/package.json
{
  "name": "app-one-mcp",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsup src/index.ts --format esm",
    "dev": "tsx watch src/index.ts",
    "start": "node dist/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "@repo/sdk": "workspace:*",
    "@repo/contracts": "workspace:*",
    "@repo/auth": "workspace:*",
    "zod": "^3.23.0"
  },
  "devDependencies": {
    "tsup": "^8.0.0",
    "tsx": "^4.0.0",
    "typescript": "^5.4.0"
  }
}
```

### Registering MCP Servers in Claude Desktop / OpenClaw

```json
// claude_desktop_config.json or openclaw config
{
  "mcpServers": {
    "app-one": {
      "command": "node",
      "args": ["./mcp-servers/app-one-mcp/dist/index.js"],
      "env": {
        "APP_ONE_API_URL": "http://localhost:3001/api",
        "APP_ONE_SERVICE_TOKEN": "${APP_ONE_SERVICE_TOKEN}"
      }
    },
    "app-two": {
      "command": "node",
      "args": ["./mcp-servers/app-two-mcp/dist/index.js"],
      "env": {
        "APP_TWO_API_URL": "http://localhost:3002/api",
        "APP_TWO_SERVICE_TOKEN": "${APP_TWO_SERVICE_TOKEN}"
      }
    }
  }
}
```

---

## Part 8 — Inter-App Communication Patterns

### Pattern 1: Synchronous API Call (via SDK)

Use when: app-two needs data from app-one in real-time, within a single request lifecycle.

```typescript
// Already shown in Part 4 — use @repo/sdk
import { createAppOneClient } from "@repo/sdk/app-one";
```

### Pattern 2: Event-Based (Webhook/Pub-Sub)

Use when: apps need to react to changes in other apps asynchronously.

```typescript
// packages/contracts/src/events/webhook.ts
import type { BaseEvent } from "./index";

export interface WebhookConfig {
  url: string;
  secret: string;
  events: string[];  // event types to subscribe to
}

// Emit event to registered webhooks
export async function emitEvent(
  event: BaseEvent,
  webhooks: WebhookConfig[]
): Promise<void> {
  const relevant = webhooks.filter((wh) =>
    wh.events.includes(event.type) || wh.events.includes("*")
  );

  await Promise.allSettled(
    relevant.map((wh) =>
      fetch(wh.url, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          "X-Webhook-Secret": wh.secret,
          "X-Event-Type": event.type,
          "X-Event-ID": event.id,
        },
        body: JSON.stringify(event),
      })
    )
  );
}
```

### Pattern 3: Shared Database Query

Use when: apps share a database and need access to the same tables.

```typescript
// Both apps import from @repo/contracts for type safety
import { SharedUserSchema } from "@repo/contracts/schemas";

// App-one writes
await supabase.schema("shared").from("users").insert(newUser);

// App-two reads
const { data } = await supabase.schema("shared").from("users").select("*");
const users = data.map((u) => SharedUserSchema.parse(u));
```

---

## Part 9 — Scaffolding Commands

### Scaffold a New App in the Monorepo

When the user says "add a new app," follow this sequence:

```bash
# 1. Create the app directory
mkdir -p apps/app-new

# 2. Scaffold based on detected framework preference
# For Next.js:
cd apps/app-new && npx create-next-app@latest . --typescript --tailwind --app --eslint --src-dir

# 3. Add shared dependencies
cd apps/app-new
pnpm add @repo/contracts @repo/auth @repo/sdk

# 4. Create the OpenAPI spec
cp packages/api-specs/_template.openapi.yaml packages/api-specs/app-new.openapi.yaml

# 5. Generate the SDK
pnpm --filter @repo/sdk generate:app-new

# 6. Scaffold the MCP server
mkdir -p mcp-servers/app-new-mcp/src/tools
# ... generate boilerplate files
```

### Scaffold a New Shared Contract

```bash
# Add a new entity type to contracts
# 1. Create the type file
touch packages/contracts/src/types/new-entity.ts

# 2. Create the schema file
touch packages/contracts/src/schemas/new-entity.schema.ts

# 3. Export from index files
# 4. Rebuild contracts
pnpm --filter @repo/contracts build
```

### Scaffold a New MCP Tool

```bash
# Add a new tool to an existing MCP server
touch mcp-servers/app-one-mcp/src/tools/new-operation.ts
# Follow the tool template from Part 7
# Register in index.ts
# Update mcp.json manifest
```

---

## Part 10 — Validation Checklist

Before considering the integration work complete, verify:

### Contracts
- [ ] All shared types are in `@repo/contracts`
- [ ] Zod schemas exist for every shared type
- [ ] No duplicate type definitions across apps
- [ ] Contracts package builds without errors

### API Specs
- [ ] OpenAPI spec exists for every app that exposes an API
- [ ] Spec passes `@redocly/cli lint`
- [ ] All shared schemas reference `@repo/contracts` types
- [ ] Error responses follow the standard `ApiError` format

### SDKs
- [ ] SDK generated from latest specs
- [ ] SDK builds without TypeScript errors
- [ ] Each app that calls another uses the SDK (not raw fetch)

### Authentication
- [ ] All apps use `@repo/auth` for token verification
- [ ] Cross-app tokens are short-lived (5 minutes max)
- [ ] `CROSS_APP_SECRET` is at least 32 characters
- [ ] Auth middleware handles both standard and cross-app tokens

### MCP Servers
- [ ] Each app has a corresponding MCP server
- [ ] Every public API endpoint has a corresponding MCP tool
- [ ] MCP tools use `@repo/sdk` (not direct API calls)
- [ ] MCP tools validate input with Zod schemas from `@repo/contracts`
- [ ] MCP manifest (`mcp.json`) is up to date

### Monorepo
- [ ] `turbo.json` has correct dependency graph
- [ ] `pnpm-workspace.yaml` includes all directories
- [ ] `pnpm turbo build` succeeds with no errors
- [ ] `pnpm turbo typecheck` succeeds with no errors

---

## Best Practices (DO)

- ALWAYS start by surveying the monorepo structure and existing packages
- ALWAYS put shared types in `@repo/contracts`, never duplicate across apps
- ALWAYS write OpenAPI specs BEFORE implementing endpoints
- ALWAYS generate SDKs from specs (never hand-write API clients)
- ALWAYS use the shared auth package for token verification
- ALWAYS scaffold MCP servers using the SDK (not raw fetch)
- ALWAYS validate MCP tool inputs with Zod schemas from contracts
- Use short-lived cross-app tokens (5 min TTL)
- Use schema-based isolation when sharing a database
- Keep MCP tools focused — one tool per operation
- Version event schemas (include `version: "1.0"` in every event)

## Anti-Patterns (AVOID)

- NEVER duplicate type definitions across apps — use `@repo/contracts`
- NEVER hand-write API clients — always generate from OpenAPI specs
- NEVER use long-lived tokens for cross-app communication
- NEVER give one app direct database access to another app's schema
- NEVER create circular dependencies between packages
- NEVER skip OpenAPI spec validation before SDK generation
- NEVER put business logic in the contracts package (types and schemas only)
- NEVER create MCP tools that bypass authentication
- NEVER hardcode base URLs — always use environment variables

## Safety Rules

- NEVER read or modify `.env`, `.env.local`, or any credential file directly
- All env var references are in generated code via `process.env.*`
- NEVER commit `CROSS_APP_SECRET` or any service tokens to git
- NEVER expose service-to-service tokens in client-side code
- NEVER create MCP tools that delete data without confirmation parameters
- NEVER auto-execute webhooks without verifying the secret
- Cross-app tokens MUST have an `aud` (audience) claim restricting which app can use them


---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### _meta.json

```json
{
  "owner": "guifav",
  "slug": "interop-forge",
  "displayName": "Inter Operations & Integration Forge",
  "latest": {
    "version": "0.1.0",
    "publishedAt": 1772054559438,
    "commit": "https://github.com/openclaw/skills/commit/0c9e54fc22b28c75572bb929ad623b453d0885b6"
  },
  "history": []
}

```

interop-forge | SkillHub