@sidurijs/hands 1.0.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,235 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
4
+ const inMemory_js_1 = require("@modelcontextprotocol/sdk/inMemory.js");
5
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
6
+ const core_1 = require("@sidurijs/core");
7
+ const index_1 = require("./index");
8
+ describe('Hands Organ - Full MCP Protocol Integration Suite', () => {
9
+ const secretKey = 'test_mcp_hands_secret';
10
+ let engine;
11
+ let sampleContext;
12
+ beforeEach(() => {
13
+ engine = new core_1.ActionPolicyEngine({
14
+ secretKey,
15
+ defaultRiskLevel: 'LOW',
16
+ });
17
+ sampleContext = {
18
+ companionId: 'comp-mcp-1',
19
+ actor: {
20
+ actorId: 'user-mcp-1',
21
+ sessionId: 'sess-mcp-1',
22
+ authorizationRole: 'operator',
23
+ capabilities: ['tool:mcp_server/echo', 'tool:mcp_server/calculator', 'tool:failing_tool', 'tool:slow_tool'],
24
+ authenticated: true,
25
+ },
26
+ conversation: {
27
+ channel: 'direct',
28
+ correlationId: 'corr-mcp-1',
29
+ },
30
+ };
31
+ });
32
+ test('dynamically discovers tools from an MCP server over transport', async () => {
33
+ const server = new index_js_1.Server({ name: 'test-mcp-srv', version: '1.0.0' }, { capabilities: { tools: {} } });
34
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
35
+ tools: [
36
+ {
37
+ name: 'echo',
38
+ description: 'Echoes back the message',
39
+ inputSchema: {
40
+ type: 'object',
41
+ properties: {
42
+ message: { type: 'string' },
43
+ },
44
+ required: ['message'],
45
+ },
46
+ },
47
+ {
48
+ name: 'calculator',
49
+ description: 'Adds two numbers',
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: {
53
+ a: { type: 'number' },
54
+ b: { type: 'number' },
55
+ },
56
+ required: ['a', 'b'],
57
+ },
58
+ },
59
+ ],
60
+ }));
61
+ const [clientTransport, serverTransport] = inMemory_js_1.InMemoryTransport.createLinkedPair();
62
+ await server.connect(serverTransport);
63
+ const hands = new index_1.DefaultHandsOrgan({
64
+ secretKey,
65
+ providers: [
66
+ {
67
+ serverName: 'mcp_server',
68
+ transport: clientTransport,
69
+ },
70
+ ],
71
+ });
72
+ const tools = await hands.listTools();
73
+ expect(tools.length).toBe(2);
74
+ expect(tools.map((t) => t.name)).toEqual(expect.arrayContaining(['echo', 'calculator']));
75
+ expect(tools.find((t) => t.name === 'echo')?.providerId).toBe('mcp_server');
76
+ await hands.close();
77
+ await server.close();
78
+ });
79
+ test('executes MCP tool call with cryptographic authorization and parameter validation', async () => {
80
+ const server = new index_js_1.Server({ name: 'calc-srv', version: '1.0.0' }, { capabilities: { tools: {} } });
81
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
82
+ tools: [
83
+ {
84
+ name: 'calculator',
85
+ description: 'Adds two numbers',
86
+ inputSchema: {
87
+ type: 'object',
88
+ properties: {
89
+ a: { type: 'number' },
90
+ b: { type: 'number' },
91
+ },
92
+ required: ['a', 'b'],
93
+ },
94
+ },
95
+ ],
96
+ }));
97
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
98
+ const args = request.params.arguments;
99
+ return {
100
+ content: [
101
+ {
102
+ type: 'text',
103
+ text: JSON.stringify({ sum: args.a + args.b }),
104
+ },
105
+ ],
106
+ };
107
+ });
108
+ const [clientTransport, serverTransport] = inMemory_js_1.InMemoryTransport.createLinkedPair();
109
+ await server.connect(serverTransport);
110
+ const hands = new index_1.DefaultHandsOrgan({
111
+ secretKey,
112
+ providers: [
113
+ {
114
+ serverName: 'mcp_server',
115
+ transport: clientTransport,
116
+ },
117
+ ],
118
+ });
119
+ // Discover tools and register in policy engine
120
+ const tools = await hands.listTools();
121
+ for (const tool of tools) {
122
+ engine.registerToolDefinition(tool);
123
+ }
124
+ // 1. Authorize action
125
+ const action = {
126
+ actionId: 'act-calc-101',
127
+ toolName: 'mcp_server/calculator',
128
+ parameters: { a: 15, b: 27 },
129
+ context: sampleContext,
130
+ };
131
+ const { capability } = await engine.evaluateAction(action);
132
+ expect(capability).toBeDefined();
133
+ // 2. Execute action through hands organ
134
+ const executionResult = await hands.executeAction(action, capability);
135
+ expect(executionResult.success).toBe(true);
136
+ expect(executionResult.lifecycle).toBe('COMPLETED');
137
+ expect(executionResult.result.content[0].text).toBe(JSON.stringify({ sum: 42 }));
138
+ await hands.close();
139
+ await server.close();
140
+ });
141
+ test('propagates MCP tool errors properly as execution failures', async () => {
142
+ const server = new index_js_1.Server({ name: 'err-srv', version: '1.0.0' }, { capabilities: { tools: {} } });
143
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
144
+ tools: [
145
+ {
146
+ name: 'failing_tool',
147
+ description: 'Fails intentionally',
148
+ inputSchema: { type: 'object' },
149
+ },
150
+ ],
151
+ }));
152
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async () => {
153
+ return {
154
+ isError: true,
155
+ content: [{ type: 'text', text: 'Database connection failed inside MCP server' }],
156
+ };
157
+ });
158
+ const [clientTransport, serverTransport] = inMemory_js_1.InMemoryTransport.createLinkedPair();
159
+ await server.connect(serverTransport);
160
+ const hands = new index_1.DefaultHandsOrgan({
161
+ secretKey,
162
+ providers: [
163
+ {
164
+ serverName: 'mcp_err',
165
+ transport: clientTransport,
166
+ },
167
+ ],
168
+ });
169
+ const tools = await hands.listTools();
170
+ for (const tool of tools) {
171
+ engine.registerToolDefinition(tool);
172
+ }
173
+ const action = {
174
+ actionId: 'act-err-1',
175
+ toolName: 'mcp_err/failing_tool',
176
+ parameters: {},
177
+ context: sampleContext,
178
+ };
179
+ const { capability } = await engine.evaluateAction(action);
180
+ const result = await hands.executeAction(action, capability);
181
+ expect(result.success).toBe(false);
182
+ expect(result.lifecycle).toBe('FAILED');
183
+ expect(result.error).toContain('Database connection failed inside MCP server');
184
+ await hands.close();
185
+ await server.close();
186
+ });
187
+ test('aborts and handles cancellation of in-flight MCP calls', async () => {
188
+ const server = new index_js_1.Server({ name: 'slow-srv', version: '1.0.0' }, { capabilities: { tools: {} } });
189
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
190
+ tools: [
191
+ {
192
+ name: 'slow_tool',
193
+ description: 'Takes long to run',
194
+ inputSchema: { type: 'object' },
195
+ },
196
+ ],
197
+ }));
198
+ let timer;
199
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async () => {
200
+ return new Promise((resolve) => {
201
+ timer = setTimeout(() => resolve({ content: [{ type: 'text', text: 'done' }] }), 200);
202
+ });
203
+ });
204
+ const [clientTransport, serverTransport] = inMemory_js_1.InMemoryTransport.createLinkedPair();
205
+ await server.connect(serverTransport);
206
+ const hands = new index_1.DefaultHandsOrgan({
207
+ secretKey,
208
+ providers: [
209
+ {
210
+ serverName: 'mcp_slow',
211
+ transport: clientTransport,
212
+ },
213
+ ],
214
+ });
215
+ const tools = await hands.listTools();
216
+ for (const tool of tools) {
217
+ engine.registerToolDefinition(tool);
218
+ }
219
+ const action = {
220
+ actionId: 'act-abort-1',
221
+ toolName: 'mcp_slow/slow_tool',
222
+ parameters: {},
223
+ context: sampleContext,
224
+ };
225
+ const { capability } = await engine.evaluateAction(action);
226
+ const controller = new AbortController();
227
+ setTimeout(() => controller.abort(), 20);
228
+ const result = await hands.executeAction(action, capability, { signal: controller.signal });
229
+ clearTimeout(timer);
230
+ expect(result.success).toBe(false);
231
+ expect(['CANCELLED', 'TIMED_OUT', 'FAILED']).toContain(result.lifecycle);
232
+ await hands.close();
233
+ await server.close();
234
+ });
235
+ });
@@ -0,0 +1,33 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
3
+ import { ToolDefinition } from '@sidurijs/core';
4
+ export interface MCPProviderConfig {
5
+ serverName: string;
6
+ baseUrl?: string;
7
+ command?: string;
8
+ args?: string[];
9
+ env?: Record<string, string>;
10
+ tools?: Array<{
11
+ definition: ToolDefinition;
12
+ execute: (parameters: Record<string, unknown>, signal?: AbortSignal) => Promise<unknown>;
13
+ }>;
14
+ transport?: Transport;
15
+ defaultTimeoutMs?: number;
16
+ }
17
+ export interface MCPToolHandler {
18
+ definition: ToolDefinition;
19
+ execute: (parameters: Record<string, unknown>, signal?: AbortSignal) => Promise<unknown>;
20
+ }
21
+ export declare class MCPClientProvider {
22
+ readonly config: MCPProviderConfig;
23
+ private client?;
24
+ private connected;
25
+ private connectingPromise?;
26
+ constructor(config: MCPProviderConfig);
27
+ get serverName(): string;
28
+ isConnected(): boolean;
29
+ getClient(): Client | undefined;
30
+ connect(): Promise<void>;
31
+ discoverTools(): Promise<MCPToolHandler[]>;
32
+ disconnect(): Promise<void>;
33
+ }
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MCPClientProvider = void 0;
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
6
+ const sse_js_1 = require("@modelcontextprotocol/sdk/client/sse.js");
7
+ class MCPClientProvider {
8
+ config;
9
+ client;
10
+ connected = false;
11
+ connectingPromise;
12
+ constructor(config) {
13
+ this.config = config;
14
+ }
15
+ get serverName() {
16
+ return this.config.serverName;
17
+ }
18
+ isConnected() {
19
+ return this.connected && !!this.client;
20
+ }
21
+ getClient() {
22
+ return this.client;
23
+ }
24
+ async connect() {
25
+ if (this.connected) {
26
+ return;
27
+ }
28
+ if (this.connectingPromise) {
29
+ return this.connectingPromise;
30
+ }
31
+ this.connectingPromise = (async () => {
32
+ let transport = this.config.transport;
33
+ if (!transport) {
34
+ if (this.config.command) {
35
+ transport = new stdio_js_1.StdioClientTransport({
36
+ command: this.config.command,
37
+ args: this.config.args,
38
+ env: this.config.env,
39
+ });
40
+ }
41
+ else if (this.config.baseUrl) {
42
+ transport = new sse_js_1.SSEClientTransport(new URL(this.config.baseUrl));
43
+ }
44
+ }
45
+ if (!transport) {
46
+ // In-memory or static tools provider (no remote transport configured)
47
+ this.connected = true;
48
+ return;
49
+ }
50
+ const client = new index_js_1.Client({
51
+ name: `siduri-x-${this.config.serverName}`,
52
+ version: '1.0.0',
53
+ }, {
54
+ capabilities: {},
55
+ });
56
+ await client.connect(transport);
57
+ this.client = client;
58
+ this.connected = true;
59
+ })();
60
+ try {
61
+ await this.connectingPromise;
62
+ }
63
+ finally {
64
+ this.connectingPromise = undefined;
65
+ }
66
+ }
67
+ async discoverTools() {
68
+ const handlers = [];
69
+ // 1. If static tools are provided in config, include them
70
+ if (this.config.tools) {
71
+ for (const tool of this.config.tools) {
72
+ handlers.push({
73
+ definition: {
74
+ ...tool.definition,
75
+ providerId: this.config.serverName,
76
+ },
77
+ execute: tool.execute,
78
+ });
79
+ }
80
+ }
81
+ // 2. If remote client is configured or connectable, discover tools dynamically via MCP
82
+ if (this.config.command || this.config.baseUrl || this.config.transport) {
83
+ if (!this.connected) {
84
+ await this.connect();
85
+ }
86
+ if (this.client) {
87
+ const response = await this.client.listTools();
88
+ for (const remoteTool of response.tools) {
89
+ const definition = {
90
+ name: remoteTool.name,
91
+ description: remoteTool.description || '',
92
+ inputSchema: remoteTool.inputSchema || { type: 'object' },
93
+ providerId: this.config.serverName,
94
+ timeoutMs: this.config.defaultTimeoutMs,
95
+ };
96
+ const execute = async (parameters, signal) => {
97
+ if (!this.client) {
98
+ throw new Error(`MCP Provider "${this.config.serverName}" is not connected`);
99
+ }
100
+ const result = await this.client.callTool({
101
+ name: remoteTool.name,
102
+ arguments: parameters,
103
+ }, undefined, { signal });
104
+ if (result.isError) {
105
+ const errorMessage = Array.isArray(result.content)
106
+ ? result.content.map((c) => ('text' in c ? c.text : JSON.stringify(c))).join('\n')
107
+ : 'Tool execution returned error status';
108
+ throw new Error(errorMessage);
109
+ }
110
+ return result;
111
+ };
112
+ handlers.push({ definition, execute });
113
+ }
114
+ }
115
+ }
116
+ return handlers;
117
+ }
118
+ async disconnect() {
119
+ if (this.client) {
120
+ try {
121
+ await this.client.close();
122
+ }
123
+ catch {
124
+ // Ignore close errors during cleanup
125
+ }
126
+ this.client = undefined;
127
+ }
128
+ this.connected = false;
129
+ }
130
+ }
131
+ exports.MCPClientProvider = MCPClientProvider;
@@ -0,0 +1,5 @@
1
+ export interface SchemaValidationResult {
2
+ valid: boolean;
3
+ errors: string[];
4
+ }
5
+ export declare function validateInputSchema(schema: Record<string, unknown> | undefined, params: Record<string, unknown> | undefined, currentPath?: string): SchemaValidationResult;
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateInputSchema = validateInputSchema;
4
+ const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
5
+ function sanitizeAndCheckForbiddenKeys(val, path = '') {
6
+ const errors = [];
7
+ if (val && typeof val === 'object') {
8
+ if (Array.isArray(val)) {
9
+ for (let i = 0; i < val.length; i++) {
10
+ errors.push(...sanitizeAndCheckForbiddenKeys(val[i], `${path}[${i}]`));
11
+ }
12
+ }
13
+ else {
14
+ const rec = val;
15
+ // Check own properties and symbol/special names
16
+ const ownKeys = Object.getOwnPropertyNames(rec);
17
+ for (const key of ownKeys) {
18
+ if (FORBIDDEN_KEYS.has(key)) {
19
+ errors.push(`Forbidden prototype pollution key "${key}" detected at path "${path ? path + '.' + key : key}"`);
20
+ }
21
+ errors.push(...sanitizeAndCheckForbiddenKeys(rec[key], path ? `${path}.${key}` : key));
22
+ }
23
+ // Check constructor or prototype property references
24
+ if (Object.prototype.hasOwnProperty.call(rec, 'constructor')) {
25
+ errors.push(`Forbidden prototype pollution key "constructor" detected at path "${path ? path + '.constructor' : 'constructor'}"`);
26
+ }
27
+ if (Object.prototype.hasOwnProperty.call(rec, '__proto__')) {
28
+ errors.push(`Forbidden prototype pollution key "__proto__" detected at path "${path ? path + '.__proto__' : '__proto__'}"`);
29
+ }
30
+ if (Object.prototype.hasOwnProperty.call(rec, 'prototype')) {
31
+ errors.push(`Forbidden prototype pollution key "prototype" detected at path "${path ? path + '.prototype' : 'prototype'}"`);
32
+ }
33
+ }
34
+ }
35
+ return errors;
36
+ }
37
+ function validateInputSchema(schema, params, currentPath = '') {
38
+ // Always inspect for prototype pollution keys recursively
39
+ const forbiddenKeyErrors = sanitizeAndCheckForbiddenKeys(params, currentPath);
40
+ if (forbiddenKeyErrors.length > 0) {
41
+ return { valid: false, errors: forbiddenKeyErrors };
42
+ }
43
+ if (!schema || Object.keys(schema).length === 0) {
44
+ return { valid: true, errors: [] };
45
+ }
46
+ const errors = [];
47
+ const parameters = params ?? {};
48
+ // Check type of parameters
49
+ if (typeof parameters !== 'object' || parameters === null || Array.isArray(parameters)) {
50
+ return { valid: false, errors: [`Parameter at "${currentPath || 'root'}" must be a JSON object`] };
51
+ }
52
+ // 1. Required fields
53
+ if (Array.isArray(schema.required)) {
54
+ for (const reqField of schema.required) {
55
+ if (typeof reqField === 'string') {
56
+ if (parameters[reqField] === undefined || parameters[reqField] === null) {
57
+ const fieldPath = currentPath ? `${currentPath}.${reqField}` : reqField;
58
+ errors.push(`Missing required parameter "${fieldPath}"`);
59
+ }
60
+ }
61
+ }
62
+ }
63
+ // 2. Recursive properties type checking
64
+ const properties = schema.properties;
65
+ if (properties && typeof properties === 'object') {
66
+ for (const [propName, propSchema] of Object.entries(properties)) {
67
+ const fieldPath = currentPath ? `${currentPath}.${propName}` : propName;
68
+ const val = parameters[propName];
69
+ if (val !== undefined && val !== null && propSchema && typeof propSchema === 'object') {
70
+ const expectedType = propSchema.type;
71
+ if (expectedType) {
72
+ if (expectedType === 'string' && typeof val !== 'string') {
73
+ errors.push(`Parameter "${fieldPath}" expected type string, got ${typeof val}`);
74
+ }
75
+ else if (expectedType === 'number' && (typeof val !== 'number' || isNaN(val))) {
76
+ errors.push(`Parameter "${fieldPath}" expected type number, got ${typeof val}`);
77
+ }
78
+ else if (expectedType === 'boolean' && typeof val !== 'boolean') {
79
+ errors.push(`Parameter "${fieldPath}" expected type boolean, got ${typeof val}`);
80
+ }
81
+ else if (expectedType === 'array') {
82
+ if (!Array.isArray(val)) {
83
+ errors.push(`Parameter "${fieldPath}" expected type array, got ${typeof val}`);
84
+ }
85
+ else if (propSchema.items && typeof propSchema.items === 'object') {
86
+ // Recursive array item validation
87
+ for (let i = 0; i < val.length; i++) {
88
+ const itemPath = `${fieldPath}[${i}]`;
89
+ const itemVal = val[i];
90
+ const itemType = propSchema.items.type;
91
+ if (itemType === 'object' || (propSchema.items.properties && typeof itemVal === 'object')) {
92
+ const nestedRes = validateInputSchema(propSchema.items, itemVal, itemPath);
93
+ if (!nestedRes.valid) {
94
+ errors.push(...nestedRes.errors);
95
+ }
96
+ }
97
+ else if (itemType && typeof itemVal !== itemType) {
98
+ errors.push(`Parameter "${itemPath}" expected type ${itemType}, got ${typeof itemVal}`);
99
+ }
100
+ }
101
+ }
102
+ }
103
+ else if (expectedType === 'object') {
104
+ if (typeof val !== 'object' || Array.isArray(val)) {
105
+ errors.push(`Parameter "${fieldPath}" expected type object, got ${typeof val}`);
106
+ }
107
+ else {
108
+ // Recursive nested object validation
109
+ const nestedRes = validateInputSchema(propSchema, val, fieldPath);
110
+ if (!nestedRes.valid) {
111
+ errors.push(...nestedRes.errors);
112
+ }
113
+ }
114
+ }
115
+ }
116
+ // Enum checking
117
+ if (Array.isArray(propSchema.enum)) {
118
+ if (!propSchema.enum.includes(val)) {
119
+ errors.push(`Parameter "${fieldPath}" has invalid value "${val}". Must be one of: [${propSchema.enum.join(', ')}]`);
120
+ }
121
+ }
122
+ }
123
+ }
124
+ // additionalProperties restriction
125
+ if (schema.additionalProperties === false) {
126
+ for (const key of Object.keys(parameters)) {
127
+ if (!properties[key]) {
128
+ const extraPath = currentPath ? `${currentPath}.${key}` : key;
129
+ errors.push(`Unexpected additional parameter "${extraPath}"`);
130
+ }
131
+ }
132
+ }
133
+ }
134
+ return {
135
+ valid: errors.length === 0,
136
+ errors,
137
+ };
138
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@sidurijs/hands",
3
+ "organType": "hands",
4
+ "version": "1.0.0",
5
+ "displayName": "Hands (MCP Tool Execution)",
6
+ "description": "Model Context Protocol tool management and cryptographically authorized action execution",
7
+ "entrypoint": "./dist/index.js",
8
+ "factory": "DefaultHandsOrgan",
9
+ "configKey": "hands",
10
+ "configSchema": {
11
+ "type": "object",
12
+ "properties": {
13
+ "defaultTimeoutMs": {
14
+ "type": "number",
15
+ "default": 10000
16
+ },
17
+ "providers": {
18
+ "type": "array",
19
+ "items": {
20
+ "type": "object",
21
+ "required": [
22
+ "serverName"
23
+ ],
24
+ "properties": {
25
+ "serverName": {
26
+ "type": "string"
27
+ },
28
+ "baseUrl": {
29
+ "type": "string"
30
+ },
31
+ "command": {
32
+ "type": "string"
33
+ },
34
+ "args": {
35
+ "type": "array",
36
+ "items": {
37
+ "type": "string"
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ },
45
+ "environment": [
46
+ {
47
+ "name": "ACTION_POLICY_SECRET",
48
+ "required": false,
49
+ "secret": true,
50
+ "description": "HMAC secret key for signing and verifying action execution capabilities (required in production)"
51
+ }
52
+ ],
53
+ "services": [],
54
+ "database": null,
55
+ "healthCheck": "probeHandsHealth"
56
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@sidurijs/hands",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "dev": "tsc -w",
9
+ "test": "jest --config jest.config.json"
10
+ },
11
+ "dependencies": {
12
+ "@modelcontextprotocol/sdk": "^1.30.0",
13
+ "@sidurijs/core": "workspace:*"
14
+ },
15
+ "devDependencies": {
16
+ "@types/jest": "^30.0.0",
17
+ "@types/node": "^26.5.1",
18
+ "jest": "^30.5.1",
19
+ "ts-jest": "^29.4.12",
20
+ "typescript": "^5.9.3"
21
+ },
22
+ "description": "Hands organ for tool execution, action policy enforcement, and MCP provider integration",
23
+ "license": "Apache-2.0",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/vxnus-studio/siduri-x",
27
+ "directory": "packages/organs/hands"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "engines": {
33
+ "node": ">=22.16.0"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "organ-manifest.json",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "exports": {
42
+ ".": {
43
+ "types": "./dist/index.d.ts",
44
+ "import": "./dist/index.js",
45
+ "default": "./dist/index.js"
46
+ },
47
+ "./organ-manifest.json": "./organ-manifest.json"
48
+ }
49
+ }