@adhd/apigen-core-client 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/CHANGELOG.md +2 -5
  2. package/README.md +259 -4
  3. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,9 +1,6 @@
1
- # Changelog
1
+ ## 0.1.1 (2026-07-23)
2
2
 
3
- All notable changes to `@adhd/apigen-core-client` will be documented in this file.
4
-
5
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
3
+ This was a version bump only for apigen-core-client to align it with other projects, there were no code changes.
7
4
 
8
5
  ## [Unreleased]
9
6
 
package/README.md CHANGED
@@ -1,19 +1,49 @@
1
1
  # @adhd/apigen-core-client
2
2
 
3
- > The core apigen engine — TypeScript source → JSON Schema extraction, composition, and the plugin contract every apigen target implements.
3
+ > The core apigen engine — TypeScript source → JSON Schema extraction, composition, the plugin contract, and the programmatic API for running live servers without code generation.
4
4
 
5
- ## What it does
6
-
7
- `@adhd/apigen-core-client` reads TypeScript source files, derives JSON Schemas for each exported function's parameters and return type, and composes them with middleware-contributed envelope fields. It defines the **v1 `OutputPlugin`** contract for code generation and the **v2 `Plugin`** capability interface (target / layer / mount / envelope) for the full plugin lifecycle. Pure TypeScript — designed for Node and the browser, though ts-morph's type resolution is Node-only. Runtime dependencies: ts-morph, ts-json-schema-generator, pino, typescript, decimal.js, and @adhd/apigen-base-logical.
5
+ `@adhd/apigen-core-client` reads TypeScript source files, derives JSON Schemas for each exported function's parameters and return type, composes them with middleware-contributed envelope fields, and provides the `OutputPlugin` contract that every plugin implements. It's the library you import when you want to **use apigen programmatically** — start MCP servers, Fastify HTTP services, or CLI tools directly from your Node.js code, without the CLI.
8
6
 
9
7
  Part of [apigen](../README.md). See the [apigen spec](../../../docs/apigen/SPEC.md) for the full architecture.
10
8
 
9
+ ---
10
+
11
+ ## Two paths: CLI or library
12
+
13
+ apigen serves two audiences through the same engine. Which one are you?
14
+
15
+ | You want to… | Use the CLI | Use this library |
16
+ |---|---|---|
17
+ | Start a server with one command, no code | `npx @adhd/apigen-cli run --source ./api.ts --type mcp --opt transport=sse` | — |
18
+ | Generate a deployable project to disk | `npx @adhd/apigen-cli generate --source ./api.ts --type api-fastify --out-dir ./out` | — |
19
+ | Embed a live server in your own Node.js process | — | `mcpPlugin.run({…})` from `@adhd/apigen-plugin-mcp` |
20
+ | Build a custom deploy pipeline with lifecycles | — | `extract()` → `composeSchemas()` → `plugin.run({ signal })` |
21
+ | Create a custom output plugin | Combined CLI + library | `OutputPlugin` / `Plugin` interface |
22
+ | Serve a multi-language polyglot front | `npx @adhd/apigen-cli serve --source ./api.ts --source ./api.py --port 8080` | — (this is a CLI-only orchestration) |
23
+
24
+ **The CLI is a thin wrapper** — every `apigen run --source ./api.ts --type mcp` call internally does exactly what you'd write in five lines of library code:
25
+
26
+ | CLI command | Library equivalent |
27
+ |---|---|
28
+ | `apigen run --source ./api.ts --type mcp` | `mcpPlugin.run({ packages: [{ id, schemas, importPath: './api.ts', fns: mod }] })` |
29
+ | `apigen run --source ./api.ts --type api-fastify --use health --use logger` | `apiFastifyPlugin.run({ packages: [...], options: { usePlugins: [healthPlugin, loggerPlugin] } })` |
30
+ | `apigen run --source ./api.ts --type mcp --opt transport=sse --opt port=3100` | `mcpPlugin.run({ packages: [...], options: { transport: 'sse', port: 3100 } })` |
31
+ | `apigen generate --source ./api.ts --type jsonschema --out-dir ./schema` | `jsonschemaPlugin.generate({ packages: [...], outputDir: './schema' })` |
32
+
33
+ **The rest of this document focuses on the library path.** See the [CLI README](../../../entrypoint/apigen-cli/README.md) for CLI usage, or the [How-To: Running Servers](./docs/how-to/running-servers.md) for a step-by-step walkthrough.
34
+
35
+ ---
36
+
11
37
  ## Install
12
38
 
13
39
  ```bash
14
40
  npm install @adhd/apigen-core-client
41
+ # Plus the plugins you want to run:
42
+ npm install @adhd/apigen-plugin-mcp @adhd/apigen-plugin-api-fastify @adhd/apigen-plugin-openapi
15
43
  ```
16
44
 
45
+ All packages are published to npm with `access: public`.
46
+
17
47
  ## Quickstart
18
48
 
19
49
  Extract canonical operation descriptors from a TypeScript file, then compose them with middleware envelope fields:
@@ -84,6 +114,229 @@ sourcesForPlugin({ language: 'ts' }, ['src/api.ts', 'src/api.py', 'README.md']);
84
114
  // => ['src/api.ts']
85
115
  ```
86
116
 
117
+ ---
118
+
119
+ ## Run Servers Programmatically
120
+
121
+ The core use case for the library path: **extract operations from your TypeScript source, then start a live server — no code generation, no CLI.**
122
+
123
+ Every plugin with a `run()` method receives a `RunInput` with three things: the composed schemas (for routing and validation), the live function references (your actual module imports), and an `AbortSignal` for graceful shutdown. The `run()` starts the server and returns a `Promise<void>` that resolves when the signal fires.
124
+
125
+ ### Start an MCP server (SSE transport)
126
+
127
+ ```ts
128
+ import { extract, composeSchemas } from '@adhd/apigen-core-client';
129
+ import { mcpPlugin } from '@adhd/apigen-plugin-mcp';
130
+
131
+ // A function — no framework imports, no decorators, no annotations
132
+ // (this lives in, say, services/api.ts)
133
+ export async function greet(name: string): Promise<string> {
134
+ return `hello, ${name}!`;
135
+ }
136
+ ```
137
+
138
+ ```ts
139
+ import { extract, composeSchemas } from '@adhd/apigen-core-client';
140
+ import { mcpPlugin } from '@adhd/apigen-plugin-mcp';
141
+
142
+ async function startMcp(sourceFile: string) {
143
+ // 1. Extract operations from your TypeScript source
144
+ const ops = await extract({ sourceFile, namespace: 'demo' });
145
+
146
+ // 2. Build generated schemas (one per exported function)
147
+ const generated = {
148
+ metadata: { namespace: 'demo', phase: '' },
149
+ schemas: Object.fromEntries(
150
+ ops.filter(o => o.kind === 'action').map(op => [
151
+ op.path[op.path.length - 1].raw,
152
+ { input: op.input, output: op.output, hasCtx: op.hasCtx, 'x-apigen-safe': op.safe },
153
+ ])
154
+ ),
155
+ };
156
+
157
+ // 3. Compose with middleware envelope (empty for this example)
158
+ const schemas = composeSchemas(generated, []);
159
+
160
+ // 4. Import the source to get live function references
161
+ const mod = await import(sourceFile);
162
+
163
+ // 5. Start the MCP server — no codegen, live dispatch
164
+ const abort = new AbortController();
165
+ process.on('SIGINT', () => abort.abort());
166
+
167
+ await mcpPlugin.run!({
168
+ packages: [{
169
+ id: 'demo',
170
+ schemas,
171
+ importPath: sourceFile,
172
+ fns: mod, // your actual exported functions
173
+ createClient: async () => ({}),
174
+ }],
175
+ outputDir: '',
176
+ options: {
177
+ transport: 'sse', // stdio | sse | streaming-http
178
+ port: 3100,
179
+ host: '0.0.0.0',
180
+ // Override tool descriptions per function (optional):
181
+ // toolDescriptions: { greet: 'Say hello to a user' },
182
+ },
183
+ signal: abort.signal,
184
+ operations: ops, // needed for --use mount plugins
185
+ });
186
+ }
187
+ ```
188
+
189
+ The MCP server is now live at `http://localhost:3100/sse` with `POST /messages?sessionId=...`. Every exported function in your source is an MCP tool — callable from Claude Desktop, Cursor, or any MCP host. **Equivalent CLI command:** `apigen run --source <file> --type mcp --opt transport=sse --opt port=3100`
190
+
191
+ ### Start a Fastify HTTP server (with OpenAPI docs)
192
+
193
+ Add the OpenAPI plugin as a `--use` mount plugin — it contributes a `GET /_meta/openapi` endpoint that serves a live OpenAPI 3.1 document derived from your operations at request time.
194
+
195
+ ```ts
196
+ import { extract, composeSchemas } from '@adhd/apigen-core-client';
197
+ import { apiFastifyPlugin } from '@adhd/apigen-plugin-api-fastify';
198
+ import { openapiPlugin } from '@adhd/apigen-plugin-openapi';
199
+ import { healthPlugin } from '@adhd/apigen-plugin-health';
200
+ import { loggerPlugin } from '@adhd/apigen-plugin-logger';
201
+
202
+ async function startFastify(sourceFile: string) {
203
+ const ops = await extract({ sourceFile, namespace: 'api' });
204
+ const generated = {
205
+ metadata: { namespace: 'api', phase: '' },
206
+ schemas: Object.fromEntries(
207
+ ops.filter(o => o.kind === 'action').map(op => [
208
+ op.path[op.path.length - 1].raw,
209
+ { input: op.input, output: op.output, hasCtx: op.hasCtx, 'x-apigen-safe': op.safe },
210
+ ])
211
+ ),
212
+ };
213
+ const schemas = composeSchemas(generated, []);
214
+ const mod = await import(sourceFile);
215
+
216
+ const abort = new AbortController();
217
+ process.on('SIGINT', () => abort.abort());
218
+
219
+ await apiFastifyPlugin.run!({
220
+ packages: [{
221
+ id: 'api',
222
+ schemas,
223
+ importPath: sourceFile,
224
+ fns: mod,
225
+ createClient: async () => ({}),
226
+ }],
227
+ outputDir: '',
228
+ options: {
229
+ port: 3200,
230
+ host: '0.0.0.0',
231
+ routePrefix: '',
232
+ // Mount plugins contributed as HTTP routes:
233
+ usePlugins: [openapiPlugin, healthPlugin, loggerPlugin],
234
+ // Per-plugin options:
235
+ // useOptions: { openapi: { title: 'My API', version: '1.0.0' } },
236
+ },
237
+ signal: abort.signal,
238
+ operations: ops,
239
+ });
240
+ }
241
+ ```
242
+
243
+ After startup:
244
+ - `GET /api/greet?name=ada` — safe operations are GET (query params)
245
+ - `POST /api/greet` — unsafe operations use `{"data":{…}}` body
246
+ - `GET /_meta/health` — contributed by `healthPlugin`
247
+ - `GET /_meta/openapi` — contributed by `openapiPlugin`, serves a live OpenAPI 3.1 spec
248
+
249
+ **Equivalent CLI command:** `apigen run --source <file> --type api-fastify --opt port=3200 --use openapi --use health --use logger`
250
+
251
+ ### Run both servers side by side
252
+
253
+ ```ts
254
+ async function startAll(sourceFile: string) {
255
+ const ops = await extract({ sourceFile, namespace: 'svc' });
256
+ const generated = {
257
+ metadata: { namespace: 'svc', phase: '' },
258
+ schemas: Object.fromEntries(
259
+ ops.filter(o => o.kind === 'action').map(op => [
260
+ op.path[op.path.length - 1].raw,
261
+ { input: op.input, output: op.output, hasCtx: op.hasCtx, 'x-apigen-safe': op.safe },
262
+ ])
263
+ ),
264
+ };
265
+ const schemas = composeSchemas(generated, []);
266
+ const mod = await import(sourceFile);
267
+ const pkg = [{
268
+ id: 'svc',
269
+ schemas,
270
+ importPath: sourceFile,
271
+ fns: mod,
272
+ createClient: async () => ({}),
273
+ }];
274
+
275
+ const abort = new AbortController();
276
+ process.on('SIGINT', () => abort.abort());
277
+
278
+ await Promise.all([
279
+ mcpPlugin.run!({ packages: pkg, outputDir: '', options: { transport: 'sse', port: 3100 }, signal: abort.signal, operations: ops }),
280
+ apiFastifyPlugin.run!({ packages: pkg, outputDir: '', options: { port: 3200, usePlugins: [openapiPlugin, healthPlugin] }, signal: abort.signal, operations: ops }),
281
+ ]);
282
+ }
283
+ ```
284
+
285
+ One source file, two servers, both live from the same import.
286
+
287
+ ### Integrate with your deploy system
288
+
289
+ Because `run()` returns a `Promise<void>` that resolves on `signal.abort`, you can wrap it in any lifecycle model — containers, process managers, custom mesh registrations.
290
+
291
+ ```ts
292
+ export async function deployService(config: {
293
+ source: string;
294
+ port: number;
295
+ type: 'mcp' | 'api-fastify';
296
+ }) {
297
+ const ops = await extract({ sourceFile: config.source, namespace: 'deploy' });
298
+ /* build schemas, import module — same pattern as above */
299
+
300
+ const plugin = config.type === 'mcp' ? mcpPlugin : apiFastifyPlugin;
301
+ const opts = config.type === 'mcp'
302
+ ? { transport: 'sse', port: config.port }
303
+ : { port: config.port, usePlugins: [openapiPlugin, healthPlugin] };
304
+
305
+ const abort = new AbortController();
306
+ // Your deployer hook: register with service mesh, health probe, etc.
307
+ const server = plugin.run!({ packages, outputDir: '', options: opts, signal: abort.signal });
308
+ return { abort, running: server }; // call abort.abort() to stop gracefully
309
+ }
310
+ ```
311
+
312
+ ### Options reference for `run()`
313
+
314
+ The `options` object in `RunInput` accepts plugin-specific keys. Here are the common ones:
315
+
316
+ | Key | Plugin(s) | Type | Default | Description |
317
+ |-----|-----------|------|---------|-------------|
318
+ | `transport` | `mcp` | `'stdio' \| 'sse' \| 'streaming-http'` | `'stdio'` | MCP transport protocol |
319
+ | `port` | `mcp` (HTTP), `api-fastify` | `number` | `3000` | Listen port |
320
+ | `host` | All | `string` | `'127.0.0.1'` | Bind address |
321
+ | `routePrefix` | `api-fastify` | `string` | `''` | Path prefix before `/<ns>/<fn>` |
322
+ | `usePlugins` | All `run()` targets | `Plugin[]` | `[]` | Layer/mount/envelope plugins to compose |
323
+ | `toolDescriptions` | `mcp` | `Record<string, string>` | `{}` | Per-tool description overrides |
324
+
325
+ ### Plugin cheat sheet
326
+
327
+ | Plugin | Package | `run()` | `generate()` | What it builds |
328
+ |--------|---------|---------|--------------|----------------|
329
+ | `mcpPlugin` | `@adhd/apigen-plugin-mcp` | ✓ | ✓ | MCP server (stdio/SSE/streaming-http) |
330
+ | `apiFastifyPlugin` | `@adhd/apigen-plugin-api-fastify` | ✓ | ✓ | Fastify HTTP server |
331
+ | `apiExpressPlugin` | `@adhd/apigen-plugin-api-express` | ✓ | ✓ | Express HTTP server |
332
+ | `cliPlugin` | `@adhd/apigen-plugin-cli-output` | ✓ | ✓ | Commander CLI tool |
333
+ | `jsonschemaPlugin` | `@adhd/apigen-plugin-jsonschema` | — | ✓ | JSON Schema files |
334
+ | `openapiPlugin` | `@adhd/apigen-plugin-openapi` | — | — | Mount: `GET /_meta/openapi` |
335
+ | `healthPlugin` | `@adhd/apigen-plugin-health` | — | — | Mount: `GET /_meta/health` |
336
+ | `loggerPlugin` | `@adhd/apigen-plugin-logger` | — | — | Layer: per-operation logging |
337
+
338
+ > See the [How-To: Running Servers](./docs/how-to/running-servers.md) for a complete walkthrough with all three transports and deployment patterns.
339
+
87
340
  ## Features
88
341
 
89
342
  ### v2 Symbol-Based Extraction
@@ -223,6 +476,8 @@ Recognized extensions: `.ts/.tsx/.mts/.cts` → `ts`, `.py` → `py`, `.rs` →
223
476
 
224
477
  ## How-To Guides
225
478
 
479
+ - [Writing Source Files for apigen](./docs/how-to/writing-source-files.md) — How to structure your TypeScript source: naming, exports, types, and what makes a good API surface
480
+ - [Running Servers Programmatically](./docs/how-to/running-servers.md) — Start MCP, Fastify, or Express servers from your own Node.js process
226
481
  - [End-to-End Extraction Pipeline](./docs/how-to/extraction-pipeline.md) — From source file to composed schemas
227
482
  - [Building apigen Plugins](./docs/how-to/building-plugins.md) — v1 OutputPlugin and v2 Plugin development
228
483
 
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@adhd/apigen-core-client",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "dependencies": {
5
5
  "ts-morph": "^23.0.0",
6
6
  "ts-json-schema-generator": "^2.3.0",
7
7
  "pino": "10.3.1",
8
8
  "typescript": "^6.0.3",
9
- "@adhd/apigen-base-logical": "^0.0.1"
9
+ "@adhd/apigen-base-logical": "^0.0.2"
10
10
  },
11
11
  "main": "./index.js",
12
12
  "module": "./index.mjs",