@eagi/sdk 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.
@@ -0,0 +1,112 @@
1
+ import type { ZodType, z } from 'zod';
2
+ import type { Identity, DomainManifest, ToolResult, HookContextMap, FilterDataMap } from '../hooks/types';
3
+ export type { Identity, DomainManifest, ToolResult };
4
+ import type { HookEngine } from '../hooks/engine';
5
+
6
+ export interface ToolContext {
7
+ identity: Identity;
8
+ domain: DomainManifest;
9
+ hooks: HookEngine;
10
+ logger: any; // Will be AuditLogger
11
+ services: Record<string, any>; // Injected domain services
12
+ tools: {
13
+ call: <T = unknown>(toolName: string, input: Record<string, unknown>) => Promise<T>;
14
+ };
15
+ resources: {
16
+ get: <T = unknown>(uri: string) => Promise<T>;
17
+ };
18
+ }
19
+
20
+ export interface ResourceContext {
21
+ identity: Identity;
22
+ domain: DomainManifest;
23
+ hooks: HookEngine;
24
+ logger: any;
25
+ services: Record<string, any>;
26
+ }
27
+
28
+ export interface ToolDefinition<TInput extends ZodType = ZodType> {
29
+ name: string;
30
+ description: string;
31
+ input: TInput;
32
+ auth?: { roles: string[] };
33
+ audit?: boolean | { redactFields?: string[] };
34
+ approval?: {
35
+ required: boolean;
36
+ message: string | ((input: z.infer<TInput>) => string);
37
+ timeout?: string;
38
+ };
39
+ handler: (input: z.infer<TInput>, ctx: ToolContext) => Promise<ToolResult>;
40
+ }
41
+
42
+ type ExtractParams<T extends string> =
43
+ T extends `${string}{${infer Param}}${infer Rest}`
44
+ ? Param | ExtractParams<Rest>
45
+ : never;
46
+
47
+ export interface ResourceDefinition<TUri extends string = string> {
48
+ uri: TUri;
49
+ name: string;
50
+ description: string;
51
+ mimeType: string;
52
+ auth?: { roles: string[] };
53
+ handler: (
54
+ params: Record<ExtractParams<TUri>, string>,
55
+ ctx: ResourceContext
56
+ ) => Promise<string>;
57
+ }
58
+
59
+ export interface PromptMessage {
60
+ role: 'user' | 'assistant';
61
+ content: { type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string };
62
+ }
63
+
64
+ export interface PromptDefinition<TArgs extends ZodType = ZodType> {
65
+ name: string;
66
+ description: string;
67
+ arguments: TArgs;
68
+ handler: (args: z.infer<TArgs>, ctx: ResourceContext) => Promise<{ messages: PromptMessage[] }>;
69
+ }
70
+
71
+ export interface DomainConfig {
72
+ env: Record<string, string | undefined>;
73
+ }
74
+
75
+ type ResolvedDeps<TDeps extends readonly string[], TServiceMap> = {
76
+ [K in TDeps[number]]: K extends keyof TServiceMap ? TServiceMap[K] : never;
77
+ };
78
+
79
+ export interface ServiceDefinition<
80
+ TName extends string = string,
81
+ TDeps extends readonly string[] = readonly string[],
82
+ TInstance = any
83
+ > {
84
+ name: TName;
85
+ deps?: TDeps;
86
+ factory: (config: DomainConfig, deps: ResolvedDeps<TDeps, any>) => Promise<TInstance>;
87
+ dispose?: (instance: TInstance) => Promise<void>;
88
+ }
89
+
90
+ export interface EagiConfig {
91
+ name: string;
92
+ version: string;
93
+ gateway?: {
94
+ port: number;
95
+ auth?: {
96
+ provider: string;
97
+ issuer: string;
98
+ audience: string;
99
+ };
100
+ };
101
+ audit?: {
102
+ hashChain?: boolean;
103
+ output?: 'file' | 'stdout';
104
+ redactInputs?: boolean;
105
+ };
106
+ hooks?: {
107
+ [K in keyof HookContextMap]?: (ctx: HookContextMap[K]) => Promise<void> | void;
108
+ } & {
109
+ [K in keyof FilterDataMap]?: (data: FilterDataMap[K], ctx?: any) => Promise<FilterDataMap[K]> | FilterDataMap[K];
110
+ };
111
+ domains?: Record<string, { enabled: boolean }>;
112
+ }
@@ -0,0 +1,38 @@
1
+ import type { ActionHandler, FilterHandler, HookContextMap, FilterDataMap } from './types';
2
+
3
+ export class HookEngine {
4
+ private actions: Map<string, Array<{ handler: ActionHandler<any>; priority: number }>> = new Map();
5
+ private filters: Map<string, Array<{ handler: FilterHandler<any>; priority: number }>> = new Map();
6
+
7
+ addAction<K extends keyof HookContextMap>(hookName: K, handler: ActionHandler<K>, priority: number = 10): void {
8
+ if (!this.actions.has(hookName)) {
9
+ this.actions.set(hookName, []);
10
+ }
11
+ this.actions.get(hookName)!.push({ handler, priority });
12
+ this.actions.get(hookName)!.sort((a, b) => a.priority - b.priority);
13
+ }
14
+
15
+ addFilter<K extends keyof FilterDataMap>(hookName: K, handler: FilterHandler<K>, priority: number = 10): void {
16
+ if (!this.filters.has(hookName)) {
17
+ this.filters.set(hookName, []);
18
+ }
19
+ this.filters.get(hookName)!.push({ handler, priority });
20
+ this.filters.get(hookName)!.sort((a, b) => a.priority - b.priority);
21
+ }
22
+
23
+ async doAction<K extends keyof HookContextMap>(hookName: K, context: HookContextMap[K]): Promise<void> {
24
+ const handlers = this.actions.get(hookName) || [];
25
+ for (const { handler } of handlers) {
26
+ await handler(context);
27
+ }
28
+ }
29
+
30
+ async applyFilters<K extends keyof FilterDataMap>(hookName: K, data: FilterDataMap[K], context?: any): Promise<FilterDataMap[K]> {
31
+ let result = data;
32
+ const handlers = this.filters.get(hookName) || [];
33
+ for (const { handler } of handlers) {
34
+ result = await handler(result, context);
35
+ }
36
+ return result;
37
+ }
38
+ }
@@ -0,0 +1,2 @@
1
+ export * from './types';
2
+ export * from './engine';
@@ -0,0 +1,45 @@
1
+ export interface Identity {
2
+ userId: string;
3
+ role: string;
4
+ email?: string;
5
+ claims: Record<string, unknown>;
6
+ }
7
+
8
+ export interface DomainManifest {
9
+ name: string;
10
+ version: string;
11
+ description?: string;
12
+ dependencies?: string[];
13
+ roles?: Record<string, { description?: string; includes?: string[] }>;
14
+ triggers?: any[];
15
+ }
16
+
17
+ export interface ToolResult {
18
+ content: Array<{ type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string }>;
19
+ isError?: boolean;
20
+ }
21
+
22
+ export interface HookContextMap {
23
+ 'before:tool:call': { toolName: string; input: unknown; identity: Identity; domain: DomainManifest };
24
+ 'after:tool:call': { toolName: string; input: unknown; output: ToolResult; identity: Identity; duration: number };
25
+ 'on:tool:error': { toolName: string; input: unknown; error: Error; identity: Identity };
26
+ 'before:resource:read': { uri: string; identity: Identity };
27
+ 'after:resource:read': { uri: string; data: string; identity: Identity };
28
+ 'on:server:start': { config: any; domains: DomainManifest[] };
29
+ 'on:server:stop': { uptime: number; requestCount: number };
30
+ 'on:domain:load': { domain: DomainManifest };
31
+ }
32
+
33
+ export interface FilterDataMap {
34
+ 'filter:tool:input': unknown;
35
+ 'filter:tool:output': ToolResult;
36
+ 'filter:resource:data': string;
37
+ 'filter:tools:list': any[]; // Will be ToolDefinition[]
38
+ 'filter:audit:entry': any; // Will be AuditEntry
39
+ }
40
+
41
+ export type ActionHandler<K extends keyof HookContextMap> = (ctx: HookContextMap[K]) => Promise<void> | void;
42
+ export type FilterHandler<K extends keyof FilterDataMap> = (
43
+ data: FilterDataMap[K],
44
+ ctx?: any
45
+ ) => Promise<FilterDataMap[K]> | FilterDataMap[K];
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './hooks';
2
+ export * from './domain';
3
+ export * from './define';
4
+ export * from './server';
5
+ export * from './middleware';
@@ -0,0 +1,30 @@
1
+ import type { ToolDefinition, ToolContext } from '../domain/types';
2
+
3
+ export class ApprovalRequiredError extends Error {
4
+ constructor(message: string) {
5
+ super(message);
6
+ this.name = 'ApprovalRequiredError';
7
+ }
8
+ }
9
+
10
+ export async function approvalMiddleware(
11
+ tool: ToolDefinition,
12
+ input: any,
13
+ ctx: ToolContext
14
+ ): Promise<void> {
15
+ if (!tool.approval || !tool.approval.required) return;
16
+
17
+ // In a real MRTR (Multi Round-Trip Request) implementation, we would check for an approval token in the request.
18
+ // For now, if the token is missing, we throw an ApprovalRequiredError which the proxy/gateway converts
19
+ // to the appropriate MCP MRTR response.
20
+
21
+ const hasApprovalToken = false; // Mock
22
+
23
+ if (!hasApprovalToken) {
24
+ const message = typeof tool.approval.message === 'function'
25
+ ? tool.approval.message(input)
26
+ : tool.approval.message;
27
+
28
+ throw new ApprovalRequiredError(message);
29
+ }
30
+ }
@@ -0,0 +1,36 @@
1
+ import type { ToolDefinition, ToolContext, ToolResult } from '../domain/types';
2
+
3
+ export async function auditMiddleware(
4
+ tool: ToolDefinition,
5
+ input: any,
6
+ output: ToolResult,
7
+ ctx: ToolContext
8
+ ): Promise<void> {
9
+ if (!tool.audit) return;
10
+
11
+ // Redaction logic based on tool.audit config
12
+ let auditedInput = { ...input };
13
+ if (typeof tool.audit === 'object' && tool.audit.redactFields) {
14
+ for (const field of tool.audit.redactFields) {
15
+ if (field in auditedInput) {
16
+ auditedInput[field] = '[REDACTED]';
17
+ }
18
+ }
19
+ }
20
+
21
+ const entry = {
22
+ timestamp: new Date().toISOString(),
23
+ tool: tool.name,
24
+ identity: ctx.identity.userId,
25
+ role: ctx.identity.role,
26
+ input: auditedInput,
27
+ success: !output.isError
28
+ };
29
+
30
+ const finalEntry = await ctx.hooks.applyFilters('filter:audit:entry', entry, ctx);
31
+
32
+ // Log it using the injected logger (which could be the gateway stream or local file)
33
+ if (ctx.logger) {
34
+ ctx.logger.info('AUDIT', finalEntry);
35
+ }
36
+ }
@@ -0,0 +1,30 @@
1
+ import type { ToolDefinition, ToolContext } from '../domain/types';
2
+
3
+ export class AuthError extends Error {
4
+ constructor(message: string) {
5
+ super(message);
6
+ this.name = 'AuthError';
7
+ }
8
+ }
9
+
10
+ export async function authMiddleware(tool: ToolDefinition, ctx: ToolContext): Promise<void> {
11
+ if (!tool.auth || !tool.auth.roles) return;
12
+
13
+ const userRole = ctx.identity.role;
14
+ const domainRoles = ctx.domain.roles || {};
15
+
16
+ const allowedRoles = new Set(tool.auth.roles);
17
+
18
+ // Basic RBAC checking
19
+ if (allowedRoles.has(userRole)) return;
20
+
21
+ // Check if user role includes any allowed role
22
+ const userRoleDef = domainRoles[userRole];
23
+ if (userRoleDef && userRoleDef.includes) {
24
+ for (const included of userRoleDef.includes) {
25
+ if (allowedRoles.has(included)) return;
26
+ }
27
+ }
28
+
29
+ throw new AuthError(`User role '${userRole}' is not authorized to execute tool '${tool.name}'`);
30
+ }
@@ -0,0 +1,3 @@
1
+ export * from './auth';
2
+ export * from './audit';
3
+ export * from './approval';
@@ -0,0 +1,185 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import {
3
+ ListToolsRequestSchema,
4
+ CallToolRequestSchema,
5
+ ListResourcesRequestSchema,
6
+ ReadResourceRequestSchema,
7
+ ListPromptsRequestSchema,
8
+ GetPromptRequestSchema,
9
+ } from '@modelcontextprotocol/sdk/types.js';
10
+ import { zodToJsonSchema } from 'zod-to-json-schema';
11
+ import type { EagiConfig, DomainRegistry, HookEngine } from '../index';
12
+ import { authMiddleware } from '../middleware/auth';
13
+ import { auditMiddleware } from '../middleware/audit';
14
+ import { approvalMiddleware } from '../middleware/approval';
15
+
16
+ export class EagiServerBuilder {
17
+ constructor(
18
+ private config: EagiConfig,
19
+ private registry: DomainRegistry,
20
+ private hooks: HookEngine,
21
+ private servicesMap: Record<string, any>
22
+ ) {}
23
+
24
+ build(): Server {
25
+ const server = new Server(
26
+ { name: this.config.name, version: this.config.version },
27
+ { capabilities: { tools: {}, resources: {}, prompts: {} } }
28
+ );
29
+
30
+ this.registerTools(server);
31
+ this.registerResources(server);
32
+ this.registerPrompts(server);
33
+
34
+ return server;
35
+ }
36
+
37
+ private registerTools(server: Server) {
38
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
39
+ // Identity projection happens at the gateway level, or here via middleware if local.
40
+ // For now, we return all tools.
41
+ const tools = this.registry.getAllTools().map(t => ({
42
+ name: t.name,
43
+ description: t.description,
44
+ inputSchema: zodToJsonSchema(t.input) as any,
45
+ }));
46
+ return { tools };
47
+ });
48
+
49
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
50
+ const { name, arguments: args } = request.params;
51
+ const tool = this.registry.getAllTools().find(t => t.name === name);
52
+
53
+ if (!tool) {
54
+ throw new Error(`Tool not found: ${name}`);
55
+ }
56
+
57
+ // Mock identity for now, normally extracted from request context (HTTP header via proxy)
58
+ const identity = { userId: 'system', role: 'admin', claims: {} };
59
+ const domain = this.registry.getAll().find(d => d.tools.includes(tool))!.manifest;
60
+
61
+ const ctx = {
62
+ identity,
63
+ domain,
64
+ hooks: this.hooks,
65
+ logger: console, // Temporary logger
66
+ services: this.servicesMap,
67
+ tools: {
68
+ call: async (toolName: string, input: any) => {
69
+ // Internal call logic
70
+ return null as any;
71
+ }
72
+ },
73
+ resources: {
74
+ get: async (uri: string) => {
75
+ return null as any;
76
+ }
77
+ }
78
+ };
79
+
80
+ let input = await this.hooks.applyFilters('filter:tool:input', args, ctx);
81
+
82
+ // Validation
83
+ const parsedInput = tool.input.parse(input);
84
+
85
+ await this.hooks.doAction('before:tool:call', { toolName: name, input: parsedInput, identity, domain });
86
+
87
+ if (tool.auth) await authMiddleware(tool, ctx);
88
+ if (tool.approval) await approvalMiddleware(tool, parsedInput, ctx);
89
+
90
+ const startTime = Date.now();
91
+ let output = await tool.handler(parsedInput, ctx);
92
+ const duration = Date.now() - startTime;
93
+
94
+ output = await this.hooks.applyFilters('filter:tool:output', output, ctx);
95
+
96
+ await this.hooks.doAction('after:tool:call', { toolName: name, input: parsedInput, output, identity, duration });
97
+
98
+ if (tool.audit) await auditMiddleware(tool, parsedInput, output, ctx);
99
+
100
+ return output as any;
101
+ });
102
+ }
103
+
104
+ private registerResources(server: Server) {
105
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
106
+ const resources = this.registry.getAllResources().map(r => ({
107
+ uri: r.uri,
108
+ name: r.name,
109
+ description: r.description,
110
+ mimeType: r.mimeType,
111
+ }));
112
+ return { resources };
113
+ });
114
+
115
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
116
+ const { uri } = request.params;
117
+ const resource = this.registry.getAllResources().find(r => r.uri === uri); // Naive matching for now
118
+
119
+ if (!resource) {
120
+ throw new Error(`Resource not found: ${uri}`);
121
+ }
122
+
123
+ const identity = { userId: 'system', role: 'admin', claims: {} };
124
+ const domain = this.registry.getAll().find(d => d.resources.includes(resource))!.manifest;
125
+
126
+ const ctx = {
127
+ identity,
128
+ domain,
129
+ hooks: this.hooks,
130
+ logger: console,
131
+ services: this.servicesMap,
132
+ };
133
+
134
+ await this.hooks.doAction('before:resource:read', { uri, identity });
135
+
136
+ let data = await resource.handler({} as any, ctx); // Naive param extraction
137
+ data = await this.hooks.applyFilters('filter:resource:data', data, ctx);
138
+
139
+ await this.hooks.doAction('after:resource:read', { uri, data, identity });
140
+
141
+ return {
142
+ contents: [{ uri, mimeType: resource.mimeType, text: data }]
143
+ };
144
+ });
145
+ }
146
+
147
+ private registerPrompts(server: Server) {
148
+ server.setRequestHandler(ListPromptsRequestSchema, async () => {
149
+ const prompts = this.registry.getAllPrompts().map(p => ({
150
+ name: p.name,
151
+ description: p.description,
152
+ arguments: [] // Should extract from Zod schema
153
+ }));
154
+ return { prompts };
155
+ });
156
+
157
+ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
158
+ const { name, arguments: args } = request.params;
159
+ const prompt = this.registry.getAllPrompts().find(p => p.name === name);
160
+
161
+ if (!prompt) {
162
+ throw new Error(`Prompt not found: ${name}`);
163
+ }
164
+
165
+ const identity = { userId: 'system', role: 'admin', claims: {} };
166
+ const domain = this.registry.getAll().find(d => d.prompts.includes(prompt))!.manifest;
167
+
168
+ const ctx = {
169
+ identity,
170
+ domain,
171
+ hooks: this.hooks,
172
+ logger: console,
173
+ services: this.servicesMap,
174
+ };
175
+
176
+ const parsedArgs = prompt.arguments.parse(args);
177
+ const result = await prompt.handler(parsedArgs, ctx);
178
+
179
+ return {
180
+ description: prompt.description,
181
+ messages: result.messages as any
182
+ };
183
+ });
184
+ }
185
+ }
@@ -0,0 +1,2 @@
1
+ export * from './builder';
2
+ export * from './lifecycle';
@@ -0,0 +1,73 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ import { EagiServerBuilder } from './builder';
3
+ import { DomainRegistry, DomainLoader, sortServices } from '../domain';
4
+ import { HookEngine } from '../hooks';
5
+ import type { EagiConfig } from '../domain/types';
6
+
7
+ export class EagiRunner {
8
+ private registry = new DomainRegistry();
9
+ private hooks = new HookEngine();
10
+ private servicesMap: Record<string, any> = {};
11
+
12
+ constructor(private config: EagiConfig) {}
13
+
14
+ async start(domainDirs: string[]) {
15
+ // 1. Register global hooks from config
16
+ if (this.config.hooks) {
17
+ for (const [hookName, handler] of Object.entries(this.config.hooks)) {
18
+ if (hookName.startsWith('filter:')) {
19
+ this.hooks.addFilter(hookName as any, handler as any);
20
+ } else {
21
+ this.hooks.addAction(hookName as any, handler as any);
22
+ }
23
+ }
24
+ }
25
+
26
+ // 2. Load domains
27
+ const loader = new DomainLoader();
28
+ for (const dir of domainDirs) {
29
+ const domain = await loader.load(dir);
30
+ this.registry.register(domain);
31
+
32
+ // Load domain-specific hooks
33
+ if (domain.hooksModule && domain.hooksModule.default) {
34
+ const domainHooks = domain.hooksModule.default;
35
+ for (const [hookName, handler] of Object.entries(domainHooks)) {
36
+ if (hookName.startsWith('filter:')) {
37
+ this.hooks.addFilter(hookName as any, handler as any);
38
+ } else {
39
+ this.hooks.addAction(hookName as any, handler as any);
40
+ }
41
+ }
42
+ }
43
+
44
+ await this.hooks.doAction('on:domain:load', { domain: domain.manifest });
45
+ }
46
+
47
+ // 3. Initialize Services (Topological Sort)
48
+ const allServices = this.registry.getAll().flatMap(d => d.services);
49
+ const sortedServices = sortServices(allServices);
50
+
51
+ for (const service of sortedServices) {
52
+ const deps: Record<string, any> = {};
53
+ if (service.deps) {
54
+ for (const dep of service.deps) {
55
+ deps[dep] = this.servicesMap[dep];
56
+ }
57
+ }
58
+ // Initialize the service factory
59
+ const instance = await service.factory({ env: process.env as any }, deps);
60
+ this.servicesMap[service.name] = instance;
61
+ }
62
+
63
+ // 4. Build MCP Server
64
+ const builder = new EagiServerBuilder(this.config, this.registry, this.hooks, this.servicesMap);
65
+ const server = builder.build();
66
+
67
+ // 5. Connect Transport
68
+ const transport = new StdioServerTransport();
69
+ await server.connect(transport);
70
+
71
+ await this.hooks.doAction('on:server:start', { config: this.config, domains: this.registry.getAll().map(d => d.manifest) });
72
+ }
73
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"]
8
+ }