@strav/mcp 0.1.0

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/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@strav/mcp",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Model Context Protocol (MCP) server for the Strav framework",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./*": "./src/*.ts"
10
+ },
11
+ "strav": {
12
+ "commands": "src/commands"
13
+ },
14
+ "files": [
15
+ "src/",
16
+ "stubs/",
17
+ "package.json",
18
+ "tsconfig.json"
19
+ ],
20
+ "dependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.12.1",
22
+ "zod": "^3.25 || ^4.0"
23
+ },
24
+ "peerDependencies": {
25
+ "@strav/kernel": "0.1.0",
26
+ "@strav/http": "0.1.0",
27
+ "@strav/cli": "0.1.0"
28
+ },
29
+ "scripts": {
30
+ "test": "bun test tests/",
31
+ "typecheck": "tsc --noEmit"
32
+ },
33
+ "devDependencies": {
34
+ "commander": "^14.0.3"
35
+ }
36
+ }
@@ -0,0 +1,114 @@
1
+ import type { Command } from 'commander'
2
+ import chalk from 'chalk'
3
+ import { bootstrap, shutdown } from '@stravigor/cli'
4
+ import McpManager from '../mcp_manager.ts'
5
+
6
+ export function register(program: Command): void {
7
+ program
8
+ .command('mcp:serve')
9
+ .description('Start the MCP server in stdio mode (for Claude Desktop, etc.)')
10
+ .action(async () => {
11
+ let db
12
+ try {
13
+ const { db: database, config } = await bootstrap()
14
+ db = database
15
+
16
+ new McpManager(config)
17
+
18
+ // Load user registration file
19
+ const registerPath = McpManager.config.register
20
+ if (registerPath) {
21
+ await import(`${process.cwd()}/${registerPath}`)
22
+ }
23
+
24
+ const tools = McpManager.registeredTools()
25
+ const resources = McpManager.registeredResources()
26
+ const prompts = McpManager.registeredPrompts()
27
+
28
+ // Log to stderr (stdout is the MCP protocol channel)
29
+ console.error(
30
+ chalk.dim(
31
+ `MCP server starting (${tools.length} tools, ${resources.length} resources, ${prompts.length} prompts)`
32
+ )
33
+ )
34
+
35
+ const { serveStdio } = await import('../transports/stdio.ts')
36
+ await serveStdio()
37
+
38
+ console.error(chalk.dim('MCP server closed.'))
39
+ } catch (err) {
40
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
41
+ process.exit(1)
42
+ } finally {
43
+ McpManager.reset()
44
+ if (db) await shutdown(db)
45
+ }
46
+ })
47
+
48
+ program
49
+ .command('mcp:list')
50
+ .description('List all registered MCP tools, resources, and prompts')
51
+ .action(async () => {
52
+ let db
53
+ try {
54
+ const { db: database, config } = await bootstrap()
55
+ db = database
56
+
57
+ new McpManager(config)
58
+
59
+ // Load user registration file
60
+ const registerPath = McpManager.config.register
61
+ if (registerPath) {
62
+ await import(`${process.cwd()}/${registerPath}`)
63
+ }
64
+
65
+ const tools = McpManager.registeredTools()
66
+ const resources = McpManager.registeredResources()
67
+ const prompts = McpManager.registeredPrompts()
68
+
69
+ console.log(chalk.bold('\nMCP Server Registry\n'))
70
+
71
+ // Tools
72
+ console.log(chalk.cyan(`Tools (${tools.length}):`))
73
+ if (tools.length === 0) {
74
+ console.log(chalk.dim(' (none)'))
75
+ } else {
76
+ for (const name of tools) {
77
+ const reg = McpManager.getToolRegistration(name)!
78
+ console.log(` ${chalk.green(name)} ${chalk.dim(reg.description ?? '')}`)
79
+ }
80
+ }
81
+ console.log()
82
+
83
+ // Resources
84
+ console.log(chalk.cyan(`Resources (${resources.length}):`))
85
+ if (resources.length === 0) {
86
+ console.log(chalk.dim(' (none)'))
87
+ } else {
88
+ for (const uri of resources) {
89
+ const reg = McpManager.getResourceRegistration(uri)!
90
+ console.log(` ${chalk.green(uri)} ${chalk.dim(reg.description ?? '')}`)
91
+ }
92
+ }
93
+ console.log()
94
+
95
+ // Prompts
96
+ console.log(chalk.cyan(`Prompts (${prompts.length}):`))
97
+ if (prompts.length === 0) {
98
+ console.log(chalk.dim(' (none)'))
99
+ } else {
100
+ for (const name of prompts) {
101
+ const reg = McpManager.getPromptRegistration(name)!
102
+ console.log(` ${chalk.green(name)} ${chalk.dim(reg.description ?? '')}`)
103
+ }
104
+ }
105
+ console.log()
106
+ } catch (err) {
107
+ console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
108
+ process.exit(1)
109
+ } finally {
110
+ McpManager.reset()
111
+ if (db) await shutdown(db)
112
+ }
113
+ })
114
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { StravError } from '@stravigor/kernel'
2
+
3
+ /** Base error class for all MCP errors. */
4
+ export class McpError extends StravError {}
5
+
6
+ /** Thrown when a tool, resource, or prompt with the same name is registered twice. */
7
+ export class DuplicateRegistrationError extends McpError {
8
+ constructor(type: 'tool' | 'resource' | 'prompt', name: string) {
9
+ super(`${type} "${name}" is already registered.`)
10
+ }
11
+ }
package/src/helpers.ts ADDED
@@ -0,0 +1,96 @@
1
+ import McpManager from './mcp_manager.ts'
2
+ import type {
3
+ ZodRawShape,
4
+ ToolHandler,
5
+ ToolRegistration,
6
+ ResourceHandler,
7
+ ResourceRegistration,
8
+ PromptHandler,
9
+ PromptRegistration,
10
+ } from './types.ts'
11
+
12
+ /**
13
+ * MCP helper — the primary convenience API.
14
+ *
15
+ * @example
16
+ * import { mcp } from '@stravigor/mcp'
17
+ * import { z } from 'zod'
18
+ *
19
+ * mcp.tool('get-user', {
20
+ * description: 'Fetch a user by ID',
21
+ * input: { id: z.number() },
22
+ * handler: async ({ id }, { app }) => {
23
+ * const db = app.resolve(Database)
24
+ * const [user] = await db.sql`SELECT * FROM users WHERE id = ${id}`
25
+ * return { content: [{ type: 'text', text: JSON.stringify(user) }] }
26
+ * }
27
+ * })
28
+ */
29
+ export const mcp = {
30
+ /** Register a tool that AI clients can invoke. */
31
+ tool<TShape extends ZodRawShape>(
32
+ name: string,
33
+ options: {
34
+ description?: string
35
+ input?: TShape
36
+ handler: ToolHandler<TShape>
37
+ }
38
+ ): void {
39
+ McpManager.tool(name, options)
40
+ },
41
+
42
+ /** Register a resource that AI clients can read. */
43
+ resource(
44
+ uri: string,
45
+ options: {
46
+ name?: string
47
+ description?: string
48
+ mimeType?: string
49
+ handler: ResourceHandler
50
+ }
51
+ ): void {
52
+ McpManager.resource(uri, options)
53
+ },
54
+
55
+ /** Register a prompt template that AI clients can use. */
56
+ prompt<TShape extends ZodRawShape>(
57
+ name: string,
58
+ options: {
59
+ description?: string
60
+ args?: TShape
61
+ handler: PromptHandler<TShape>
62
+ }
63
+ ): void {
64
+ McpManager.prompt(name, options)
65
+ },
66
+
67
+ /** List all registered tool names. */
68
+ registeredTools(): string[] {
69
+ return McpManager.registeredTools()
70
+ },
71
+
72
+ /** List all registered resource URIs. */
73
+ registeredResources(): string[] {
74
+ return McpManager.registeredResources()
75
+ },
76
+
77
+ /** List all registered prompt names. */
78
+ registeredPrompts(): string[] {
79
+ return McpManager.registeredPrompts()
80
+ },
81
+
82
+ /** Get a tool registration by name. */
83
+ getToolRegistration(name: string): ToolRegistration | undefined {
84
+ return McpManager.getToolRegistration(name)
85
+ },
86
+
87
+ /** Get a resource registration by URI. */
88
+ getResourceRegistration(uri: string): ResourceRegistration | undefined {
89
+ return McpManager.getResourceRegistration(uri)
90
+ },
91
+
92
+ /** Get a prompt registration by name. */
93
+ getPromptRegistration(name: string): PromptRegistration | undefined {
94
+ return McpManager.getPromptRegistration(name)
95
+ },
96
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ // Manager
2
+ export { default, default as McpManager } from './mcp_manager.ts'
3
+
4
+ // Provider
5
+ export { default as McpProvider } from './mcp_provider.ts'
6
+ export type { McpProviderOptions } from './mcp_provider.ts'
7
+
8
+ // Helper
9
+ export { mcp } from './helpers.ts'
10
+
11
+ // Transports
12
+ export { serveStdio } from './transports/stdio.ts'
13
+ export { mountHttpTransport } from './transports/bun_http_transport.ts'
14
+ export type { WebStandardStreamableHTTPServerTransport } from './transports/bun_http_transport.ts'
15
+
16
+ // Errors
17
+ export { McpError, DuplicateRegistrationError } from './errors.ts'
18
+
19
+ // Types
20
+ export type {
21
+ McpConfig,
22
+ ZodRawShape,
23
+ InferShape,
24
+ ToolHandlerContext,
25
+ ToolRegistration,
26
+ ToolHandler,
27
+ ResourceRegistration,
28
+ ResourceHandler,
29
+ PromptRegistration,
30
+ PromptHandler,
31
+ // Re-exported SDK result types
32
+ CallToolResult,
33
+ GetPromptResult,
34
+ ReadResourceResult,
35
+ } from './types.ts'
@@ -0,0 +1,264 @@
1
+ import { inject, app, Configuration, Emitter, ConfigurationError } from '@stravigor/kernel'
2
+ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
3
+ import type {
4
+ McpConfig,
5
+ ZodRawShape,
6
+ ToolRegistration,
7
+ ToolHandler,
8
+ ToolHandlerContext,
9
+ ResourceRegistration,
10
+ ResourceHandler,
11
+ PromptRegistration,
12
+ PromptHandler,
13
+ } from './types.ts'
14
+ import { DuplicateRegistrationError } from './errors.ts'
15
+
16
+ @inject
17
+ export default class McpManager {
18
+ private static _config: McpConfig
19
+ private static _server: McpServer | null = null
20
+ private static _tools = new Map<string, ToolRegistration>()
21
+ private static _resources = new Map<string, ResourceRegistration>()
22
+ private static _prompts = new Map<string, PromptRegistration>()
23
+
24
+ constructor(config: Configuration) {
25
+ McpManager._config = {
26
+ name: (config.get('mcp.name') ?? config.get('app.name', 'Strav MCP Server')) as string,
27
+ version: config.get('mcp.version', '1.0.0') as string,
28
+ register: config.get('mcp.register') as string | undefined,
29
+ http: {
30
+ enabled: config.get('mcp.http.enabled', true) as boolean,
31
+ path: config.get('mcp.http.path', '/mcp') as string,
32
+ },
33
+ }
34
+ }
35
+
36
+ // ── Configuration ──────────────────────────────────────────────────
37
+
38
+ static get config(): McpConfig {
39
+ if (!McpManager._config) {
40
+ throw new ConfigurationError(
41
+ 'McpManager not configured. Resolve it through the container first.'
42
+ )
43
+ }
44
+ return McpManager._config
45
+ }
46
+
47
+ // ── Builder API ────────────────────────────────────────────────────
48
+
49
+ /** Register a tool that AI clients can invoke. */
50
+ static tool<TShape extends ZodRawShape>(
51
+ name: string,
52
+ options: {
53
+ description?: string
54
+ input?: TShape
55
+ handler: ToolHandler<TShape>
56
+ }
57
+ ): void {
58
+ if (McpManager._tools.has(name)) {
59
+ throw new DuplicateRegistrationError('tool', name)
60
+ }
61
+
62
+ McpManager._tools.set(name, {
63
+ name,
64
+ description: options.description,
65
+ input: options.input,
66
+ handler: options.handler as ToolHandler,
67
+ })
68
+
69
+ Emitter.emit('mcp:tool-registered', { name })
70
+ }
71
+
72
+ /** Register a resource that AI clients can read. */
73
+ static resource(
74
+ uri: string,
75
+ options: {
76
+ name?: string
77
+ description?: string
78
+ mimeType?: string
79
+ handler: ResourceHandler
80
+ }
81
+ ): void {
82
+ if (McpManager._resources.has(uri)) {
83
+ throw new DuplicateRegistrationError('resource', uri)
84
+ }
85
+
86
+ McpManager._resources.set(uri, {
87
+ uri,
88
+ name: options.name,
89
+ description: options.description,
90
+ mimeType: options.mimeType,
91
+ handler: options.handler,
92
+ })
93
+
94
+ Emitter.emit('mcp:resource-registered', { uri })
95
+ }
96
+
97
+ /** Register a prompt template that AI clients can use. */
98
+ static prompt<TShape extends ZodRawShape>(
99
+ name: string,
100
+ options: {
101
+ description?: string
102
+ args?: TShape
103
+ handler: PromptHandler<TShape>
104
+ }
105
+ ): void {
106
+ if (McpManager._prompts.has(name)) {
107
+ throw new DuplicateRegistrationError('prompt', name)
108
+ }
109
+
110
+ McpManager._prompts.set(name, {
111
+ name,
112
+ description: options.description,
113
+ args: options.args,
114
+ handler: options.handler as PromptHandler,
115
+ })
116
+
117
+ Emitter.emit('mcp:prompt-registered', { name })
118
+ }
119
+
120
+ // ── Server ─────────────────────────────────────────────────────────
121
+
122
+ /**
123
+ * Get or create the MCP server instance.
124
+ *
125
+ * Lazily creates the server and wires all registered tools, resources,
126
+ * and prompts. Handlers are wrapped to inject the Application context.
127
+ */
128
+ static getServer(): McpServer {
129
+ if (McpManager._server) return McpManager._server
130
+
131
+ const server = new McpServer({
132
+ name: McpManager.config.name,
133
+ version: McpManager.config.version,
134
+ })
135
+
136
+ const ctx: ToolHandlerContext = { app }
137
+
138
+ // Wire tools — cast at SDK boundary since our handler signature
139
+ // adds the DI context param that the SDK doesn't know about
140
+ for (const [name, reg] of McpManager._tools) {
141
+ const toolCb = async (params: any) => {
142
+ const result = await reg.handler(params ?? {}, ctx)
143
+ await Emitter.emit('mcp:tool-called', { name, params })
144
+ return result
145
+ }
146
+
147
+ if (reg.input) {
148
+ server.registerTool(
149
+ name,
150
+ {
151
+ description: reg.description,
152
+ inputSchema: reg.input,
153
+ },
154
+ toolCb as any
155
+ )
156
+ } else {
157
+ server.registerTool(
158
+ name,
159
+ {
160
+ description: reg.description,
161
+ },
162
+ toolCb as any
163
+ )
164
+ }
165
+ }
166
+
167
+ // Wire resources
168
+ for (const [, reg] of McpManager._resources) {
169
+ const isTemplate = reg.uri.includes('{')
170
+ const metadata = {
171
+ title: reg.name,
172
+ description: reg.description,
173
+ mimeType: reg.mimeType,
174
+ }
175
+
176
+ if (isTemplate) {
177
+ server.registerResource(
178
+ reg.name ?? reg.uri,
179
+ new ResourceTemplate(reg.uri, { list: undefined }),
180
+ metadata,
181
+ (async (uri: URL, params: Record<string, string>) => {
182
+ const result = await reg.handler(uri, params, ctx)
183
+ await Emitter.emit('mcp:resource-read', { uri: uri.href })
184
+ return result
185
+ }) as any
186
+ )
187
+ } else {
188
+ server.registerResource(reg.name ?? reg.uri, reg.uri, metadata, (async (uri: URL) => {
189
+ const result = await reg.handler(uri, {}, ctx)
190
+ await Emitter.emit('mcp:resource-read', { uri: uri.href })
191
+ return result
192
+ }) as any)
193
+ }
194
+ }
195
+
196
+ // Wire prompts
197
+ for (const [name, reg] of McpManager._prompts) {
198
+ const promptCb = async (args: any) => {
199
+ const result = await reg.handler(args ?? {}, ctx)
200
+ await Emitter.emit('mcp:prompt-called', { name, args })
201
+ return result
202
+ }
203
+
204
+ if (reg.args) {
205
+ server.registerPrompt(
206
+ name,
207
+ {
208
+ description: reg.description,
209
+ argsSchema: reg.args,
210
+ },
211
+ promptCb as any
212
+ )
213
+ } else {
214
+ server.registerPrompt(
215
+ name,
216
+ {
217
+ description: reg.description,
218
+ },
219
+ promptCb as any
220
+ )
221
+ }
222
+ }
223
+
224
+ McpManager._server = server
225
+ return server
226
+ }
227
+
228
+ // ── Inspection ─────────────────────────────────────────────────────
229
+
230
+ static registeredTools(): string[] {
231
+ return Array.from(McpManager._tools.keys())
232
+ }
233
+
234
+ static registeredResources(): string[] {
235
+ return Array.from(McpManager._resources.keys())
236
+ }
237
+
238
+ static registeredPrompts(): string[] {
239
+ return Array.from(McpManager._prompts.keys())
240
+ }
241
+
242
+ static getToolRegistration(name: string): ToolRegistration | undefined {
243
+ return McpManager._tools.get(name)
244
+ }
245
+
246
+ static getResourceRegistration(uri: string): ResourceRegistration | undefined {
247
+ return McpManager._resources.get(uri)
248
+ }
249
+
250
+ static getPromptRegistration(name: string): PromptRegistration | undefined {
251
+ return McpManager._prompts.get(name)
252
+ }
253
+
254
+ // ── Reset ──────────────────────────────────────────────────────────
255
+
256
+ /** Reset all state. Intended for test teardown. */
257
+ static reset(): void {
258
+ McpManager._tools.clear()
259
+ McpManager._resources.clear()
260
+ McpManager._prompts.clear()
261
+ McpManager._server = null
262
+ McpManager._config = undefined as any
263
+ }
264
+ }
@@ -0,0 +1,43 @@
1
+ import { ServiceProvider } from '@stravigor/kernel'
2
+ import type { Application } from '@stravigor/kernel'
3
+ import { Router } from '@stravigor/http'
4
+ import McpManager from './mcp_manager.ts'
5
+ import { mountHttpTransport } from './transports/bun_http_transport.ts'
6
+
7
+ export interface McpProviderOptions {
8
+ /** Auto-mount HTTP transport on the router. Default: `true` */
9
+ mountHttp?: boolean
10
+ }
11
+
12
+ export default class McpProvider extends ServiceProvider {
13
+ readonly name = 'mcp'
14
+ override readonly dependencies = ['config']
15
+
16
+ constructor(private options?: McpProviderOptions) {
17
+ super()
18
+ }
19
+
20
+ override register(app: Application): void {
21
+ app.singleton(McpManager)
22
+ }
23
+
24
+ override async boot(app: Application): Promise<void> {
25
+ app.resolve(McpManager)
26
+
27
+ // Load user registration file if configured
28
+ const registerPath = McpManager.config.register
29
+ if (registerPath) {
30
+ await import(`${process.cwd()}/${registerPath}`)
31
+ }
32
+
33
+ // Mount HTTP transport on the router
34
+ if (this.options?.mountHttp !== false && McpManager.config.http.enabled) {
35
+ const router = app.resolve(Router)
36
+ mountHttpTransport(router)
37
+ }
38
+ }
39
+
40
+ override shutdown(): void {
41
+ McpManager.reset()
42
+ }
43
+ }
@@ -0,0 +1,42 @@
1
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
2
+ import type { Router, Context } from '@stravigor/http'
3
+ import { Emitter } from '@stravigor/kernel'
4
+ import McpManager from '../mcp_manager.ts'
5
+
6
+ export type { WebStandardStreamableHTTPServerTransport }
7
+
8
+ /**
9
+ * Mount the MCP server on a Stravigor router via Streamable HTTP.
10
+ *
11
+ * Uses the SDK's `WebStandardStreamableHTTPServerTransport` which works
12
+ * natively with Bun's Web Standard Request/Response API.
13
+ *
14
+ * Registers handlers at the configured path (default: `/mcp`) for
15
+ * POST (requests), GET (SSE), and DELETE (session termination).
16
+ */
17
+ export function mountHttpTransport(router: Router): WebStandardStreamableHTTPServerTransport {
18
+ const path = McpManager.config.http.path
19
+
20
+ const transport = new WebStandardStreamableHTTPServerTransport({
21
+ sessionIdGenerator: () => crypto.randomUUID(),
22
+ })
23
+
24
+ const server = McpManager.getServer()
25
+ server.connect(transport)
26
+
27
+ // The SDK transport handles POST/GET/DELETE routing internally
28
+ // via handleRequest(req: Request): Promise<Response>.
29
+ const handler = async (ctx: Context) => {
30
+ const response = await transport.handleRequest(ctx.request)
31
+ await Emitter.emit('mcp:http-request', { method: ctx.method, path })
32
+ return response
33
+ }
34
+
35
+ router.post(path, handler)
36
+ router.get(path, handler)
37
+ router.delete(path, handler)
38
+
39
+ Emitter.emit('mcp:http-mounted', { path })
40
+
41
+ return transport
42
+ }
@@ -0,0 +1,30 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
2
+ import { Emitter } from '@stravigor/kernel'
3
+ import McpManager from '../mcp_manager.ts'
4
+
5
+ /**
6
+ * Start the MCP server in stdio mode.
7
+ *
8
+ * Reads JSON-RPC messages from stdin, writes responses to stdout.
9
+ * Blocks until the AI client disconnects.
10
+ *
11
+ * Used by the `mcp:serve` CLI command. Claude Desktop spawns
12
+ * the process and communicates over stdio.
13
+ */
14
+ export async function serveStdio(): Promise<void> {
15
+ const server = McpManager.getServer()
16
+ const transport = new StdioServerTransport()
17
+
18
+ await Emitter.emit('mcp:stdio-starting')
19
+
20
+ await server.connect(transport)
21
+
22
+ await Emitter.emit('mcp:stdio-connected')
23
+
24
+ // Block until the transport closes (AI client disconnects)
25
+ await new Promise<void>(resolve => {
26
+ transport.onclose = () => {
27
+ Emitter.emit('mcp:stdio-closed').then(resolve)
28
+ }
29
+ })
30
+ }
package/src/types.ts ADDED
@@ -0,0 +1,86 @@
1
+ import type { z } from 'zod'
2
+ import type { Application } from '@stravigor/kernel'
3
+ import type {
4
+ CallToolResult,
5
+ GetPromptResult,
6
+ ReadResourceResult,
7
+ } from '@modelcontextprotocol/sdk/types.js'
8
+
9
+ // Re-export SDK result types for user convenience
10
+ export type { CallToolResult, GetPromptResult, ReadResourceResult }
11
+
12
+ // ── Configuration ────────────────────────────────────────────────────
13
+
14
+ export interface McpConfig {
15
+ /** Server name shown to AI clients. Defaults to app name. */
16
+ name: string
17
+ /** Server version. */
18
+ version: string
19
+ /** Path to a file that registers tools/resources/prompts (for CLI). */
20
+ register?: string
21
+ /** HTTP transport settings. */
22
+ http: {
23
+ /** Whether to enable HTTP transport. */
24
+ enabled: boolean
25
+ /** Mount path for the MCP endpoint. */
26
+ path: string
27
+ }
28
+ }
29
+
30
+ // ── Handler Context ──────────────────────────────────────────────────
31
+
32
+ export interface ToolHandlerContext {
33
+ /** The Application container — resolve any service via DI. */
34
+ app: Application
35
+ }
36
+
37
+ // ── Tool ─────────────────────────────────────────────────────────────
38
+
39
+ /** Raw Zod shape: `{ id: z.number(), name: z.string() }`. */
40
+ export type ZodRawShape = Record<string, z.ZodTypeAny>
41
+
42
+ /** Infer the output type from a Zod raw shape. */
43
+ export type InferShape<T extends ZodRawShape> = { [K in keyof T]: z.infer<T[K]> }
44
+
45
+ export interface ToolRegistration<TShape extends ZodRawShape = ZodRawShape> {
46
+ name: string
47
+ description?: string
48
+ input?: TShape
49
+ handler: ToolHandler<TShape>
50
+ }
51
+
52
+ export type ToolHandler<TShape extends ZodRawShape = ZodRawShape> = (
53
+ params: InferShape<TShape>,
54
+ ctx: ToolHandlerContext
55
+ ) => CallToolResult | Promise<CallToolResult>
56
+
57
+ // ── Resource ─────────────────────────────────────────────────────────
58
+
59
+ export interface ResourceRegistration {
60
+ /** URI or URI template, e.g., `'file:///config'` or `'strav://models/{name}'`. */
61
+ uri: string
62
+ name?: string
63
+ description?: string
64
+ mimeType?: string
65
+ handler: ResourceHandler
66
+ }
67
+
68
+ export type ResourceHandler = (
69
+ uri: URL,
70
+ params: Record<string, string>,
71
+ ctx: ToolHandlerContext
72
+ ) => ReadResourceResult | Promise<ReadResourceResult>
73
+
74
+ // ── Prompt ───────────────────────────────────────────────────────────
75
+
76
+ export interface PromptRegistration<TShape extends ZodRawShape = ZodRawShape> {
77
+ name: string
78
+ description?: string
79
+ args?: TShape
80
+ handler: PromptHandler<TShape>
81
+ }
82
+
83
+ export type PromptHandler<TShape extends ZodRawShape = ZodRawShape> = (
84
+ args: InferShape<TShape>,
85
+ ctx: ToolHandlerContext
86
+ ) => GetPromptResult | Promise<GetPromptResult>
@@ -0,0 +1,24 @@
1
+ import { env } from '@stravigor/kernel/helpers'
2
+
3
+ export default {
4
+ /** Server name shown to AI clients. Defaults to app name if not set. */
5
+ name: env('MCP_NAME', undefined),
6
+
7
+ /** Server version. */
8
+ version: env('MCP_VERSION', '1.0.0'),
9
+
10
+ /**
11
+ * Path to the file that registers tools, resources, and prompts.
12
+ * This file is imported automatically by the provider and CLI commands.
13
+ */
14
+ register: 'mcp/server.ts',
15
+
16
+ /** HTTP transport settings (for hosted deployments). */
17
+ http: {
18
+ /** Enable the HTTP transport. Mounts an MCP endpoint on the router. */
19
+ enabled: env('MCP_HTTP', 'true').bool(),
20
+
21
+ /** Mount path for the MCP endpoint. */
22
+ path: env('MCP_PATH', '/mcp'),
23
+ },
24
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": ["src/**/*.ts"],
4
+ "exclude": ["node_modules", "tests"]
5
+ }