@mostajs/net 1.0.0-alpha.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.
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ // @mostajs/net — Multi-protocol transport layer for @mostajs/orm
2
+ // Author: Dr Hamid MADANI drmdh@msn.com
3
+ // Config
4
+ export { loadNetConfig, getEnabledTransports, TRANSPORT_NAMES } from './core/config.js';
5
+ // Factory
6
+ export { getTransport, getActiveTransports, stopAllTransports } from './core/factory.js';
7
+ // Middleware
8
+ export { composeMiddleware, loggingMiddleware } from './core/middleware.js';
9
+ // Transports
10
+ export { RestTransport } from './transports/rest.transport.js';
11
+ export { SSETransport } from './transports/sse.transport.js';
12
+ export { GraphQLTransport } from './transports/graphql.transport.js';
13
+ export { WebSocketTransport } from './transports/ws.transport.js';
14
+ export { JsonRpcTransport } from './transports/jsonrpc.transport.js';
15
+ export { McpTransport } from './transports/mcp.transport.js';
16
+ // Auth / API Keys
17
+ export { readApiKeys, writeApiKeys, generateApiKey, hashApiKey, createSubscription, revokeSubscription, validateApiKey, checkPermission, } from './auth/apikeys.js';
18
+ export { apiKeyMiddleware } from './auth/apikey-middleware.js';
19
+ // Server
20
+ export { startServer } from './server.js';
@@ -0,0 +1,18 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import { EntityService } from '@mostajs/orm';
3
+ export interface NetServer {
4
+ app: FastifyInstance;
5
+ entityService: EntityService;
6
+ stop: () => Promise<void>;
7
+ }
8
+ /**
9
+ * Start the @mostajs/net server.
10
+ *
11
+ * 1. Load config from .env.local (MOSTA_NET_*)
12
+ * 2. Connect to ORM via @mostajs/orm (reads MOSTA_ORM_* / DB_DIALECT + SGBD_URI)
13
+ * 3. Create EntityService
14
+ * 4. Load and start enabled transports
15
+ * 5. Wire transports to EntityService
16
+ * 6. Listen on MOSTA_NET_PORT
17
+ */
18
+ export declare function startServer(): Promise<NetServer>;
package/dist/server.js ADDED
@@ -0,0 +1,221 @@
1
+ // @mostajs/net — Main server orchestrator
2
+ // Loads config, connects ORM, starts transports, wires everything together
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ import Fastify from 'fastify';
5
+ import { EntityService, getAllSchemas, getDialect } from '@mostajs/orm';
6
+ import { loadNetConfig, getEnabledTransports } from './core/config.js';
7
+ import { getTransport, stopAllTransports } from './core/factory.js';
8
+ import { loggingMiddleware } from './core/middleware.js';
9
+ import { RestTransport } from './transports/rest.transport.js';
10
+ import { SSETransport } from './transports/sse.transport.js';
11
+ import { GraphQLTransport } from './transports/graphql.transport.js';
12
+ import { WebSocketTransport } from './transports/ws.transport.js';
13
+ import { JsonRpcTransport } from './transports/jsonrpc.transport.js';
14
+ import { McpTransport } from './transports/mcp.transport.js';
15
+ /**
16
+ * Start the @mostajs/net server.
17
+ *
18
+ * 1. Load config from .env.local (MOSTA_NET_*)
19
+ * 2. Connect to ORM via @mostajs/orm (reads MOSTA_ORM_* / DB_DIALECT + SGBD_URI)
20
+ * 3. Create EntityService
21
+ * 4. Load and start enabled transports
22
+ * 5. Wire transports to EntityService
23
+ * 6. Listen on MOSTA_NET_PORT
24
+ */
25
+ export async function startServer() {
26
+ // 1. Load net config
27
+ const config = loadNetConfig();
28
+ // 2. Connect ORM
29
+ const dialect = await getDialect();
30
+ // 3. Create EntityService
31
+ const entityService = new EntityService(dialect);
32
+ // 4. Get schemas
33
+ const schemas = getAllSchemas();
34
+ // 5. Create shared Fastify instance
35
+ const app = Fastify({ logger: false });
36
+ // 6. ORM handler (OrmRequest → EntityService → OrmResponse)
37
+ const ormHandler = async (req, _ctx) => {
38
+ return entityService.execute(req);
39
+ };
40
+ // 7. Load and start enabled transports
41
+ const enabledNames = getEnabledTransports(config);
42
+ for (const name of enabledNames) {
43
+ const transportConfig = config.transports[name];
44
+ const transport = await getTransport(name, transportConfig);
45
+ if (!transport)
46
+ continue;
47
+ // Register all schemas
48
+ for (const schema of schemas) {
49
+ transport.registerEntity(schema);
50
+ }
51
+ // Add logging middleware
52
+ transport.use(loggingMiddleware);
53
+ // Wire ORM handler
54
+ if (transport instanceof RestTransport) {
55
+ transport.setHandler(ormHandler);
56
+ }
57
+ // Start the transport
58
+ await transport.start(transportConfig);
59
+ // Mount REST routes directly on the shared Fastify instance
60
+ if (transport instanceof RestTransport) {
61
+ for (const schema of schemas) {
62
+ registerRestRoutes(app, schema, ormHandler);
63
+ }
64
+ }
65
+ // Mount SSE route and wire EntityService events → broadcast
66
+ if (transport instanceof SSETransport) {
67
+ const sseTransport = transport;
68
+ const ssePath = sseTransport.getPath();
69
+ // SSE endpoint: GET /events
70
+ app.get(ssePath, async (req, reply) => {
71
+ // Use raw Node.js response for SSE streaming
72
+ reply.hijack();
73
+ sseTransport.addClient(reply.raw);
74
+ });
75
+ // Wire EntityService change events → SSE broadcast
76
+ entityService.on('entity.created', (data) => sseTransport.broadcast('entity.created', data));
77
+ entityService.on('entity.updated', (data) => sseTransport.broadcast('entity.updated', data));
78
+ entityService.on('entity.deleted', (data) => sseTransport.broadcast('entity.deleted', data));
79
+ entityService.on('entity.upserted', (data) => sseTransport.broadcast('entity.upserted', data));
80
+ }
81
+ // Mount GraphQL via mercurius
82
+ if (transport instanceof GraphQLTransport) {
83
+ const gqlTransport = transport;
84
+ gqlTransport.setHandler(ormHandler);
85
+ const schema = gqlTransport.generateSchema();
86
+ const resolvers = gqlTransport.generateResolvers();
87
+ const mercurius = (await import('mercurius')).default;
88
+ await app.register(mercurius, {
89
+ schema,
90
+ resolvers,
91
+ path: gqlTransport.getInfo().url || '/graphql',
92
+ graphiql: true, // Enable GraphiQL IDE at the same path
93
+ });
94
+ }
95
+ // Mount JSON-RPC endpoint
96
+ if (transport instanceof JsonRpcTransport) {
97
+ const rpcTransport = transport;
98
+ rpcTransport.setHandler(ormHandler);
99
+ const rpcPath = rpcTransport.getPath();
100
+ app.post(rpcPath, async (req, reply) => {
101
+ const result = await rpcTransport.handleBody(req.body);
102
+ return result;
103
+ });
104
+ // Discovery: list available methods
105
+ app.get(rpcPath, async () => ({
106
+ jsonrpc: '2.0',
107
+ methods: rpcTransport.listMethods(),
108
+ }));
109
+ }
110
+ // Mount MCP endpoint (Streamable HTTP)
111
+ if (transport instanceof McpTransport) {
112
+ const mcpTransport = transport;
113
+ mcpTransport.setHandler(ormHandler);
114
+ const mcpPath = mcpTransport.getPath();
115
+ // MCP uses raw Node.js request/response for streaming
116
+ app.all(mcpPath, async (req, reply) => {
117
+ reply.hijack();
118
+ await mcpTransport.handleRequest(req.raw, reply.raw, req.body);
119
+ });
120
+ }
121
+ const info = transport.getInfo();
122
+ console.log(` \x1b[32m●\x1b[0m ${info.name.charAt(0).toUpperCase() + info.name.slice(1)}Transport${info.url ? ' ' + info.url : ''}${info.port ? ' :' + info.port : ''}`);
123
+ }
124
+ // 8. Health check
125
+ app.get('/health', async () => ({ status: 'ok', transports: enabledNames, entities: schemas.map(s => s.name) }));
126
+ // 9. Listen
127
+ await app.listen({ port: config.port, host: '0.0.0.0' });
128
+ // 10. Attach WebSocket to the HTTP server (must be after listen)
129
+ for (const name of enabledNames) {
130
+ const transport = (await import('./core/factory.js')).getActiveTransports().find(t => t.name === name);
131
+ if (transport instanceof WebSocketTransport) {
132
+ const wsTransport = transport;
133
+ wsTransport.setHandler(ormHandler);
134
+ wsTransport.attachToServer(app.server);
135
+ // Wire change events → WS broadcast
136
+ entityService.on('entity.created', (data) => wsTransport.broadcast('entity.created', data));
137
+ entityService.on('entity.updated', (data) => wsTransport.broadcast('entity.updated', data));
138
+ entityService.on('entity.deleted', (data) => wsTransport.broadcast('entity.deleted', data));
139
+ }
140
+ }
141
+ console.log(`\n \x1b[36mReady.\x1b[0m ${schemas.length} entities × ${enabledNames.length} transports = ${schemas.length * enabledNames.length} endpoints\n`);
142
+ return {
143
+ app,
144
+ entityService,
145
+ stop: async () => {
146
+ await stopAllTransports();
147
+ await app.close();
148
+ },
149
+ };
150
+ }
151
+ /**
152
+ * Register REST routes directly on the main Fastify instance.
153
+ * This avoids the complexity of merging two Fastify instances.
154
+ */
155
+ function registerRestRoutes(app, schema, ormHandler) {
156
+ const prefix = '/api/v1';
157
+ const col = schema.collection;
158
+ const handle = async (ormReq, reply) => {
159
+ const ctx = { transport: 'rest' };
160
+ const res = await ormHandler(ormReq, ctx);
161
+ if (res.status === 'error') {
162
+ reply.status(res.error?.code === 'ENTITY_NOT_FOUND' || res.error?.code === 'EntityNotFoundError' ? 404 :
163
+ res.error?.code?.startsWith('MISSING') ? 400 : 500);
164
+ }
165
+ return res;
166
+ };
167
+ // GET /api/v1/{collection}
168
+ app.get(`${prefix}/${col}`, async (req, reply) => {
169
+ const q = req.query;
170
+ return handle({
171
+ op: 'findAll', entity: schema.name,
172
+ filter: q.filter ? JSON.parse(q.filter) : {},
173
+ options: {
174
+ sort: q.sort ? JSON.parse(q.sort) : undefined,
175
+ limit: q.limit ? parseInt(q.limit, 10) : undefined,
176
+ skip: q.skip ? parseInt(q.skip, 10) : undefined,
177
+ },
178
+ }, reply);
179
+ });
180
+ // GET /api/v1/{collection}/count
181
+ app.get(`${prefix}/${col}/count`, async (req, reply) => {
182
+ const q = req.query;
183
+ return handle({ op: 'count', entity: schema.name, filter: q.filter ? JSON.parse(q.filter) : {} }, reply);
184
+ });
185
+ // GET /api/v1/{collection}/:id
186
+ app.get(`${prefix}/${col}/:id`, async (req, reply) => {
187
+ const { id } = req.params;
188
+ const q = req.query;
189
+ return handle({
190
+ op: 'findById', entity: schema.name, id,
191
+ relations: q.include ? q.include.split(',') : undefined,
192
+ }, reply);
193
+ });
194
+ // POST /api/v1/{collection}
195
+ app.post(`${prefix}/${col}`, async (req, reply) => {
196
+ const res = await handle({ op: 'create', entity: schema.name, data: req.body }, reply);
197
+ if (reply.statusCode < 400)
198
+ reply.status(201);
199
+ return res;
200
+ });
201
+ // PUT /api/v1/{collection}/:id
202
+ app.put(`${prefix}/${col}/:id`, async (req, reply) => {
203
+ const { id } = req.params;
204
+ return handle({ op: 'update', entity: schema.name, id, data: req.body }, reply);
205
+ });
206
+ // DELETE /api/v1/{collection}/:id
207
+ app.delete(`${prefix}/${col}/:id`, async (req, reply) => {
208
+ const { id } = req.params;
209
+ return handle({ op: 'delete', entity: schema.name, id }, reply);
210
+ });
211
+ // POST /api/v1/{collection}/search
212
+ app.post(`${prefix}/${col}/search`, async (req, reply) => {
213
+ const body = req.body;
214
+ return handle({
215
+ op: 'search', entity: schema.name,
216
+ query: body.query,
217
+ searchFields: body.fields,
218
+ options: body.options,
219
+ }, reply);
220
+ });
221
+ }
@@ -0,0 +1,29 @@
1
+ import type { EntitySchema, OrmRequest, OrmResponse } from '@mostajs/orm';
2
+ import type { ITransport, TransportConfig, TransportInfo, TransportMiddleware, TransportContext } from '../core/types.js';
3
+ type OrmHandler = (req: OrmRequest, ctx: TransportContext) => Promise<OrmResponse>;
4
+ export declare class GraphQLTransport implements ITransport {
5
+ readonly name = "graphql";
6
+ private config;
7
+ private schemas;
8
+ private middlewares;
9
+ private ormHandler;
10
+ private stats;
11
+ setHandler(handler: OrmHandler): void;
12
+ use(mw: TransportMiddleware): void;
13
+ registerEntity(schema: EntitySchema): void;
14
+ start(config: TransportConfig): Promise<void>;
15
+ stop(): Promise<void>;
16
+ getInfo(): TransportInfo;
17
+ /**
18
+ * Generate the GraphQL SDL schema string from registered EntitySchemas.
19
+ */
20
+ generateSchema(): string;
21
+ /**
22
+ * Generate resolvers that call OrmHandler.
23
+ */
24
+ generateResolvers(): Record<string, Record<string, Function>>;
25
+ private callOrm;
26
+ }
27
+ /** Factory */
28
+ export declare function createTransport(): ITransport;
29
+ export {};
@@ -0,0 +1,164 @@
1
+ // GraphQLTransport — Auto-generates GraphQL schema from EntitySchemas
2
+ // Queries and mutations call EntityService via OrmRequest/OrmResponse
3
+ // Author: Dr Hamid MADANI drmdh@msn.com
4
+ export class GraphQLTransport {
5
+ name = 'graphql';
6
+ config = null;
7
+ schemas = [];
8
+ middlewares = [];
9
+ ormHandler = null;
10
+ stats = { requests: 0, errors: 0, startedAt: 0 };
11
+ setHandler(handler) { this.ormHandler = handler; }
12
+ use(mw) { this.middlewares.push(mw); }
13
+ registerEntity(schema) { this.schemas.push(schema); }
14
+ async start(config) {
15
+ this.config = config;
16
+ this.stats.startedAt = Date.now();
17
+ }
18
+ async stop() { this.config = null; }
19
+ getInfo() {
20
+ return {
21
+ name: this.name,
22
+ status: this.config ? 'running' : 'stopped',
23
+ url: this.config?.path || '/graphql',
24
+ entities: this.schemas.map(s => s.name),
25
+ stats: { ...this.stats },
26
+ };
27
+ }
28
+ /**
29
+ * Generate the GraphQL SDL schema string from registered EntitySchemas.
30
+ */
31
+ generateSchema() {
32
+ const types = [];
33
+ const queries = [];
34
+ const mutations = [];
35
+ for (const schema of this.schemas) {
36
+ const name = schema.name;
37
+ const fields = Object.entries(schema.fields);
38
+ // Type definition
39
+ const fieldDefs = [
40
+ ' id: ID!',
41
+ ...fields.map(([fname, fdef]) => ` ${fname}: ${gqlType(fdef.type, fdef.required)}`),
42
+ ...(schema.timestamps ? [' createdAt: String', ' updatedAt: String'] : []),
43
+ ];
44
+ types.push(`type ${name} {\n${fieldDefs.join('\n')}\n}`);
45
+ // Input type for create/update
46
+ const inputFields = fields.map(([fname, fdef]) => ` ${fname}: ${gqlInputType(fdef.type, fdef.required && fname !== 'id')}`);
47
+ types.push(`input ${name}Input {\n${inputFields.join('\n')}\n}`);
48
+ // Queries
49
+ queries.push(` ${lcfirst(name)}s(filter: String, limit: Int, skip: Int, sort: String): [${name}!]!`);
50
+ queries.push(` ${lcfirst(name)}(id: ID!): ${name}`);
51
+ queries.push(` ${lcfirst(name)}Count(filter: String): Int!`);
52
+ // Mutations
53
+ mutations.push(` create${name}(input: ${name}Input!): ${name}!`);
54
+ mutations.push(` update${name}(id: ID!, input: ${name}Input!): ${name}`);
55
+ mutations.push(` delete${name}(id: ID!): Boolean!`);
56
+ }
57
+ return [
58
+ ...types,
59
+ `type Query {\n${queries.join('\n')}\n}`,
60
+ `type Mutation {\n${mutations.join('\n')}\n}`,
61
+ ].join('\n\n');
62
+ }
63
+ /**
64
+ * Generate resolvers that call OrmHandler.
65
+ */
66
+ generateResolvers() {
67
+ const Query = {};
68
+ const Mutation = {};
69
+ for (const schema of this.schemas) {
70
+ const name = schema.name;
71
+ // Query: users(filter, limit, skip, sort)
72
+ Query[`${lcfirst(name)}s`] = async (_, args) => {
73
+ this.stats.requests++;
74
+ const req = {
75
+ op: 'findAll',
76
+ entity: name,
77
+ filter: args.filter ? JSON.parse(args.filter) : {},
78
+ options: {
79
+ limit: args.limit,
80
+ skip: args.skip,
81
+ sort: args.sort ? JSON.parse(args.sort) : undefined,
82
+ },
83
+ };
84
+ const res = await this.callOrm(req);
85
+ return res.data || [];
86
+ };
87
+ // Query: user(id)
88
+ Query[lcfirst(name)] = async (_, args) => {
89
+ this.stats.requests++;
90
+ const res = await this.callOrm({ op: 'findById', entity: name, id: args.id });
91
+ return res.data;
92
+ };
93
+ // Query: userCount(filter)
94
+ Query[`${lcfirst(name)}Count`] = async (_, args) => {
95
+ this.stats.requests++;
96
+ const res = await this.callOrm({
97
+ op: 'count', entity: name,
98
+ filter: args.filter ? JSON.parse(args.filter) : {},
99
+ });
100
+ return res.data || 0;
101
+ };
102
+ // Mutation: createUser(input)
103
+ Mutation[`create${name}`] = async (_, args) => {
104
+ this.stats.requests++;
105
+ const res = await this.callOrm({ op: 'create', entity: name, data: args.input });
106
+ if (res.status === 'error')
107
+ throw new Error(res.error?.message);
108
+ return res.data;
109
+ };
110
+ // Mutation: updateUser(id, input)
111
+ Mutation[`update${name}`] = async (_, args) => {
112
+ this.stats.requests++;
113
+ const res = await this.callOrm({ op: 'update', entity: name, id: args.id, data: args.input });
114
+ if (res.status === 'error')
115
+ throw new Error(res.error?.message);
116
+ return res.data;
117
+ };
118
+ // Mutation: deleteUser(id)
119
+ Mutation[`delete${name}`] = async (_, args) => {
120
+ this.stats.requests++;
121
+ const res = await this.callOrm({ op: 'delete', entity: name, id: args.id });
122
+ return res.data === true;
123
+ };
124
+ }
125
+ return { Query, Mutation };
126
+ }
127
+ async callOrm(req) {
128
+ if (!this.ormHandler) {
129
+ return { status: 'error', error: { code: 'NO_HANDLER', message: 'ORM handler not initialized' } };
130
+ }
131
+ const ctx = { transport: this.name };
132
+ return this.ormHandler(req, ctx);
133
+ }
134
+ }
135
+ // ============================================================
136
+ // Helpers
137
+ // ============================================================
138
+ function gqlType(type, required) {
139
+ const base = fieldTypeToGql(type);
140
+ return required ? `${base}!` : base;
141
+ }
142
+ function gqlInputType(type, required) {
143
+ const base = fieldTypeToGql(type);
144
+ return required ? `${base}!` : base;
145
+ }
146
+ function fieldTypeToGql(type) {
147
+ switch (type) {
148
+ case 'string':
149
+ case 'text': return 'String';
150
+ case 'number': return 'Float';
151
+ case 'boolean': return 'Boolean';
152
+ case 'date': return 'String';
153
+ case 'json': return 'String';
154
+ case 'array': return '[String]';
155
+ default: return 'String';
156
+ }
157
+ }
158
+ function lcfirst(s) {
159
+ return s.charAt(0).toLowerCase() + s.slice(1);
160
+ }
161
+ /** Factory */
162
+ export function createTransport() {
163
+ return new GraphQLTransport();
164
+ }
@@ -0,0 +1,41 @@
1
+ import type { EntitySchema, OrmRequest, OrmResponse } from '@mostajs/orm';
2
+ import type { ITransport, TransportConfig, TransportInfo, TransportMiddleware, TransportContext } from '../core/types.js';
3
+ type OrmHandler = (req: OrmRequest, ctx: TransportContext) => Promise<OrmResponse>;
4
+ interface JsonRpcResponse {
5
+ jsonrpc: '2.0';
6
+ result?: unknown;
7
+ error?: {
8
+ code: number;
9
+ message: string;
10
+ data?: unknown;
11
+ };
12
+ id: string | number | null;
13
+ }
14
+ export declare class JsonRpcTransport implements ITransport {
15
+ readonly name = "jsonrpc";
16
+ private config;
17
+ private schemas;
18
+ private middlewares;
19
+ private ormHandler;
20
+ private stats;
21
+ setHandler(handler: OrmHandler): void;
22
+ use(mw: TransportMiddleware): void;
23
+ registerEntity(schema: EntitySchema): void;
24
+ start(config: TransportConfig): Promise<void>;
25
+ stop(): Promise<void>;
26
+ getInfo(): TransportInfo;
27
+ getPath(): string;
28
+ /**
29
+ * Handle a JSON-RPC 2.0 request body.
30
+ * Supports single requests and batch (array of requests).
31
+ */
32
+ handleBody(body: unknown): Promise<JsonRpcResponse | JsonRpcResponse[]>;
33
+ private handleSingleRequest;
34
+ /**
35
+ * List available methods (for discovery).
36
+ */
37
+ listMethods(): string[];
38
+ }
39
+ /** Factory */
40
+ export declare function createTransport(): ITransport;
41
+ export {};
@@ -0,0 +1,116 @@
1
+ // JsonRpcTransport — JSON-RPC 2.0 over HTTP
2
+ // Implements the JSON-RPC 2.0 specification (jsonrpc.org)
3
+ // Foundation for MCP transport (MCP uses JSON-RPC 2.0)
4
+ // Author: Dr Hamid MADANI drmdh@msn.com
5
+ // JSON-RPC 2.0 error codes
6
+ const PARSE_ERROR = -32700;
7
+ const INVALID_REQUEST = -32600;
8
+ const METHOD_NOT_FOUND = -32601;
9
+ const INVALID_PARAMS = -32602;
10
+ const INTERNAL_ERROR = -32603;
11
+ export class JsonRpcTransport {
12
+ name = 'jsonrpc';
13
+ config = null;
14
+ schemas = [];
15
+ middlewares = [];
16
+ ormHandler = null;
17
+ stats = { requests: 0, errors: 0, startedAt: 0 };
18
+ setHandler(handler) { this.ormHandler = handler; }
19
+ use(mw) { this.middlewares.push(mw); }
20
+ registerEntity(schema) { this.schemas.push(schema); }
21
+ async start(config) {
22
+ this.config = config;
23
+ this.stats.startedAt = Date.now();
24
+ }
25
+ async stop() { this.config = null; }
26
+ getInfo() {
27
+ return {
28
+ name: this.name,
29
+ status: this.config ? 'running' : 'stopped',
30
+ url: this.config?.path || '/rpc',
31
+ entities: this.schemas.map(s => s.name),
32
+ stats: { ...this.stats },
33
+ };
34
+ }
35
+ getPath() {
36
+ return this.config?.path || '/rpc';
37
+ }
38
+ /**
39
+ * Handle a JSON-RPC 2.0 request body.
40
+ * Supports single requests and batch (array of requests).
41
+ */
42
+ async handleBody(body) {
43
+ // Batch request
44
+ if (Array.isArray(body)) {
45
+ return Promise.all(body.map(req => this.handleSingleRequest(req)));
46
+ }
47
+ return this.handleSingleRequest(body);
48
+ }
49
+ async handleSingleRequest(raw) {
50
+ this.stats.requests++;
51
+ // Validate JSON-RPC envelope
52
+ const req = raw;
53
+ if (!req || req.jsonrpc !== '2.0' || typeof req.method !== 'string') {
54
+ this.stats.errors++;
55
+ return { jsonrpc: '2.0', error: { code: INVALID_REQUEST, message: 'Invalid JSON-RPC 2.0 request' }, id: req?.id ?? null };
56
+ }
57
+ // Parse method: "entity.{op}" or "{entityName}.{op}"
58
+ const parts = req.method.split('.');
59
+ if (parts.length < 2) {
60
+ this.stats.errors++;
61
+ return { jsonrpc: '2.0', error: { code: METHOD_NOT_FOUND, message: `Invalid method format: "${req.method}". Expected: "Entity.operation"` }, id: req.id ?? null };
62
+ }
63
+ const entityName = parts[0];
64
+ const op = parts[1];
65
+ const params = (req.params || {});
66
+ // Build OrmRequest
67
+ const ormReq = {
68
+ op: op,
69
+ entity: entityName,
70
+ id: params.id,
71
+ filter: params.filter,
72
+ data: params.data,
73
+ options: params.options,
74
+ relations: params.relations,
75
+ query: params.query,
76
+ searchFields: params.searchFields,
77
+ stages: params.stages,
78
+ };
79
+ if (!this.ormHandler) {
80
+ this.stats.errors++;
81
+ return { jsonrpc: '2.0', error: { code: INTERNAL_ERROR, message: 'ORM handler not initialized' }, id: req.id ?? null };
82
+ }
83
+ const ctx = { transport: this.name };
84
+ const res = await this.ormHandler(ormReq, ctx);
85
+ if (res.status === 'error') {
86
+ this.stats.errors++;
87
+ return {
88
+ jsonrpc: '2.0',
89
+ error: { code: INTERNAL_ERROR, message: res.error?.message || 'Unknown error', data: res.error },
90
+ id: req.id ?? null,
91
+ };
92
+ }
93
+ return {
94
+ jsonrpc: '2.0',
95
+ result: { data: res.data, metadata: res.metadata },
96
+ id: req.id ?? null,
97
+ };
98
+ }
99
+ /**
100
+ * List available methods (for discovery).
101
+ */
102
+ listMethods() {
103
+ const methods = [];
104
+ const ops = ['findAll', 'findOne', 'findById', 'create', 'update', 'delete', 'count', 'search'];
105
+ for (const schema of this.schemas) {
106
+ for (const op of ops) {
107
+ methods.push(`${schema.name}.${op}`);
108
+ }
109
+ }
110
+ return methods;
111
+ }
112
+ }
113
+ /** Factory */
114
+ export function createTransport() {
115
+ return new JsonRpcTransport();
116
+ }
@@ -0,0 +1,34 @@
1
+ import type { IncomingMessage, ServerResponse } from 'http';
2
+ import type { EntitySchema, OrmRequest, OrmResponse } from '@mostajs/orm';
3
+ import type { ITransport, TransportConfig, TransportInfo, TransportMiddleware, TransportContext } from '../core/types.js';
4
+ type OrmHandler = (req: OrmRequest, ctx: TransportContext) => Promise<OrmResponse>;
5
+ export declare class McpTransport implements ITransport {
6
+ readonly name = "mcp";
7
+ private config;
8
+ private schemas;
9
+ private middlewares;
10
+ private ormHandler;
11
+ private stats;
12
+ setHandler(handler: OrmHandler): void;
13
+ use(mw: TransportMiddleware): void;
14
+ registerEntity(schema: EntitySchema): void;
15
+ start(config: TransportConfig): Promise<void>;
16
+ stop(): Promise<void>;
17
+ getInfo(): TransportInfo;
18
+ getPath(): string;
19
+ /**
20
+ * Handle an incoming MCP HTTP request.
21
+ * Called by the main Fastify server via route handler.
22
+ */
23
+ /**
24
+ * Handle an incoming MCP HTTP request.
25
+ * Creates a new McpServer + Transport per session for stateless mode.
26
+ */
27
+ handleRequest(req: IncomingMessage, res: ServerResponse, body?: unknown): Promise<void>;
28
+ private registerEntityToolsOn;
29
+ private registerEntityResourcesOn;
30
+ private callOrm;
31
+ }
32
+ /** Factory */
33
+ export declare function createTransport(): ITransport;
34
+ export {};