Skip to content

aipartnerup/nestjs-apcore

Repository files navigation

nestjs-apcore

NestJS adapter for the apcore AI-Perceivable module ecosystem. Turn your existing NestJS services into AI-callable MCP tools and OpenAI-compatible function definitions — with zero changes to your business logic.

Features

  • Decorator-driven — Mark methods with @ApTool and classes with @ApModule to expose them as AI tools
  • Auto-discoveryApToolScannerService scans all providers at startup and registers decorated methods automatically
  • Multi-schema support — TypeBox, Zod, class-validator DTOs, and plain JSON Schema, auto-detected via a priority chain
  • MCP server built-in — Serve tools over stdio, Streamable HTTP, or SSE transports with optional Tool Explorer UI
  • OpenAI-compatible — Convert registered tools to OpenAI function-calling format with toOpenaiTools()
  • YAML bindings — Register tools declaratively from YAML files without touching source code
  • Dual access — Services remain injectable into REST controllers while simultaneously available as MCP tools

Installation

npm install nestjs-apcore apcore-js apcore-mcp @modelcontextprotocol/sdk js-yaml

Peer dependencies (required):

npm install @nestjs/common @nestjs/core reflect-metadata rxjs

Optional (for schema adapters):

npm install zod                                # ZodAdapter
npm install class-validator class-transformer  # DtoAdapter
npm install @sinclair/typebox                  # TypeBoxAdapter (recommended)

Requirements: Node.js >= 18, NestJS >= 10

Quick Start

1. Wire up modules

// app.module.ts
import { Module } from '@nestjs/common';
import { ApcoreModule, ApcoreMcpModule, ApToolScannerService } from 'nestjs-apcore';

@Module({
  imports: [
    ApcoreModule.forRoot({}),
    ApcoreMcpModule.forRoot({
      transport: 'streamable-http',
      port: 8000,
      name: 'my-app',
      version: '1.0.0',
      explorer: true,
    }),
    TodoModule,
  ],
  providers: [ApToolScannerService],
})
export class AppModule {}

2. Decorate your services

// todo.service.ts
import { Injectable } from '@nestjs/common';
import { Type } from '@sinclair/typebox';
import { ApModule, ApTool } from 'nestjs-apcore';

@ApModule({ namespace: 'todo', description: 'Todo list management' })
@Injectable()
export class TodoService {
  private todos = [];

  @ApTool({
    description: 'List all todos, optionally filtered by status',
    inputSchema: Type.Object({
      done: Type.Optional(Type.Boolean()),
    }),
    annotations: { readonly: true, idempotent: true },
    tags: ['todo', 'query'],
  })
  list(inputs: Record<string, unknown>) {
    const { done } = inputs;
    const filtered = done !== undefined
      ? this.todos.filter((t) => t.done === done)
      : this.todos;
    return { todos: filtered, count: filtered.length };
  }

  @ApTool({
    description: 'Create a new todo item',
    inputSchema: Type.Object({
      title: Type.String(),
    }),
    annotations: { readonly: false },
    tags: ['todo', 'mutate'],
  })
  create(inputs: Record<string, unknown>) {
    const todo = { id: this.todos.length + 1, title: inputs.title, done: false };
    this.todos.push(todo);
    return todo;
  }
}

3. Boot the app

// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
  // MCP server starts automatically on port 8000
}
bootstrap();

Your service is now available as:

  • REST API at http://localhost:3000 (via NestJS controllers)
  • MCP server at http://localhost:8000 (for AI agents)
  • Tool Explorer at http://localhost:8000/explorer/ (interactive web UI)

API Reference

Modules

ApcoreModule

Provides the core Registry and Executor as NestJS singletons.

// Sync
ApcoreModule.forRoot({ extensionsDir?, acl?, middleware?, bindings? })

// Async (e.g. inject ConfigService)
ApcoreModule.forRootAsync({ imports, useFactory, inject })

ApcoreMcpModule

Configures and starts the MCP server.

The MCP server runs standalone on its own port (e.g. 8000), separate from NestJS's REST server (e.g. 3000). Both share the same Registry and Executor instances.

ApcoreMcpModule.forRoot({
  transport: 'stdio' | 'streamable-http' | 'sse',
  host?: string,
  port?: number,
  name?: string,
  version?: string,
  tags?: string[],          // only expose tools matching these tags
  prefix?: string,          // only expose tools with this ID prefix
  explorer?: boolean,       // enable Tool Explorer web UI
  explorerPrefix?: string,  // URL prefix for the Explorer
  allowExecute?: boolean,   // allow execution from Explorer
  dynamic?: boolean,        // enable dynamic tool list updates
  validateInputs?: boolean,
  logLevel?: 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' | 'CRITICAL',
  authenticator?: Authenticator,   // JWT or custom auth
  exemptPaths?: string[],          // paths that bypass auth (default: ['/health', '/metrics'])
  metricsCollector?: { exportPrometheus(): string },
  onStartup?: () => void | Promise<void>,
  onShutdown?: () => void | Promise<void>,
})

Decorators

Decorator Target Description
@ApModule(opts) Class Sets namespace for all @ApTool methods (e.g. todo)
@ApTool(opts) Method Registers the method as an AI-callable tool
@ApContext() Parameter Injects the apcore Context object (callerId, trace info)

@ApTool options

@ApTool({
  description: string,        // required
  id?: string,                 // auto-generated in snake_case if omitted (e.g. 'email.send_batch')
  inputSchema?: any,           // TypeBox, Zod, DTO class, or JSON Schema
  outputSchema?: any,
  annotations?: {
    readonly?: boolean,
    destructive?: boolean,
    idempotent?: boolean,
    requiresApproval?: boolean,
    openWorld?: boolean,
    streaming?: boolean,
  },
  tags?: string[],
  documentation?: string,
  examples?: ApToolExample[],
})

Services

Service Description
ApcoreRegistryService Register/unregister tools, query the registry
ApcoreExecutorService Execute tools: call(), stream(), validate()
ApcoreMcpService MCP server lifecycle: start(), stop(), restart(), toOpenaiTools()
ApToolScannerService Auto-discovers and registers @ApTool decorated methods at startup

Schema Adapters

Schemas are auto-detected and converted via a priority chain:

Adapter Priority Input
TypeBoxAdapter 100 @sinclair/typebox schemas
ZodAdapter 50 Zod schemas
JsonSchemaAdapter 30 Plain JSON Schema objects
DtoAdapter 20 class-validator decorated DTO classes

YAML Bindings

Register tools without decorators:

bindings:
  - module_id: email.send
    target: EmailService.send
    description: Send an email
    input_schema:
      type: object
      properties:
        to: { type: string }
        subject: { type: string }
        body: { type: string }
    tags: [email, mutate]
    annotations:
      readonly: false

JWT Authentication

Enable JWT auth by passing a JWTAuthenticator to ApcoreMcpModule. The Explorer UI and /health endpoint are always exempt.

import { JWTAuthenticator, getCurrentIdentity } from 'nestjs-apcore';

// In app.module.ts
const jwtSecret = process.env.JWT_SECRET;
ApcoreMcpModule.forRoot({
  transport: 'streamable-http',
  port: 8000,
  authenticator: jwtSecret ? new JWTAuthenticator({ secret: jwtSecret }) : undefined,
})

// Inside any @ApTool method
list(inputs: Record<string, unknown>) {
  const caller = getCurrentIdentity()?.id ?? 'anonymous';
  return { items: [...], caller };
}

JWTAuthenticator options: secret, algorithms (default ['HS256']), audience, issuer, claimMapping, requireClaims, requireAuth (default true).

Re-exported Utilities

From apcore-mcp:

  • reportProgress, elicit, createBridgeContext
  • JWTAuthenticator, getCurrentIdentity, identityStorage

Examples

The demo/ directory contains a full working app with Todo and Weather services:

cd demo
npm install
npx tsx src/main.ts

Or with Docker:

cd demo
docker compose up --build

Scripts

Command Description
npm run build Compile TypeScript
npm run dev Watch mode compilation
npm test Run tests (vitest)
npm run test:watch Run tests in watch mode
npm run test:coverage Run tests with coverage
npm run typecheck Type-check without emitting
npm run lint Lint source and tests

License

Apache-2.0

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors