Skip to content

Commit 2e100a3

Browse files
committed
feat: resources
1 parent 1c01c77 commit 2e100a3

13 files changed

+328
-48
lines changed

src/mcp/mcp.module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { SessionManagerService } from './services/session-manager.service';
44
import { McpServerFactory } from './services/mcp-server.factory';
55
import { McpProtocolHandlerService } from './services/mcp-protocol-handler.service';
66
import { ToolsModule } from '../tools/tools.module';
7+
import { ResourcesModule } from '../resources/resources.module';
78
import { AuthModule } from '../auth/auth.module';
89
import { CommonModule } from '../common/common.module';
910

@@ -17,10 +18,12 @@ import { CommonModule } from '../common/common.module';
1718
* - In-memory session management (PoC)
1819
* - JWT-based authentication
1920
* - Tool registry integration
21+
* - Resource registry for documentation
2022
*/
2123
@Module({
2224
imports: [
2325
ToolsModule, // For tool registration
26+
ResourcesModule, // For resource registration
2427
AuthModule, // For JWT validation
2528
CommonModule, // For shared utilities and decorators
2629
],

src/mcp/services/mcp-protocol-handler.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ export class McpProtocolHandlerService {
124124
tools: {
125125
listChanged: false,
126126
},
127+
resources: {
128+
listChanged: false,
129+
},
127130
},
128131
serverInfo: {
129132
name: 'iot-cloud-mcp-gateway',

src/mcp/services/mcp-server.factory.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
22
import { ConfigService } from '@nestjs/config';
33
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
44
import { ToolRegistryService } from '../../tools/services/tool-registry.service';
5+
import { ResourceRegistryService } from '../../resources/services/resource-registry.service';
56

67
/**
78
* McpServerFactory
@@ -14,6 +15,7 @@ export class McpServerFactory {
1415

1516
constructor(
1617
private readonly toolRegistry: ToolRegistryService,
18+
private readonly resourceRegistry: ResourceRegistryService,
1719
private readonly configService: ConfigService,
1820
) {}
1921

@@ -34,13 +36,17 @@ export class McpServerFactory {
3436
{
3537
capabilities: {
3638
tools: {},
39+
resources: {},
3740
},
3841
},
3942
);
4043

4144
// Register all available tools on this server instance
4245
this.toolRegistry.registerTools(server, projectApiKey);
4346

47+
// Register all available resources
48+
this.resourceRegistry.registerResources(server);
49+
4450

4551
this.logger.log(`MCP server created and tools registered for project: ${projectApiKey}`);
4652

src/proxy/services/iot-api.service.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export class IotApiService {
7373
logProxyCall('Login successful', { email });
7474
return response.data;
7575
} catch (error) {
76-
logProxyCall('Login failed', { error: (error as Error).message });
76+
this.logDetailedError('Login', error);
7777
throw error;
7878
}
7979
}
@@ -103,7 +103,7 @@ export class IotApiService {
103103
logProxyCall('Auth code registered successfully', { userId });
104104
return response.data;
105105
} catch (error) {
106-
logProxyCall('Auth code registration failed', { error: (error as Error).message });
106+
this.logDetailedError('Register auth code', error);
107107
throw error;
108108
}
109109
}
@@ -133,7 +133,7 @@ export class IotApiService {
133133
logProxyCall('Auth code exchange successful');
134134
return response.data;
135135
} catch (error) {
136-
logProxyCall('Auth code exchange failed', { error: (error as Error).message });
136+
this.logDetailedError('Exchange auth code', error);
137137
throw error;
138138
}
139139
}
@@ -163,7 +163,7 @@ export class IotApiService {
163163
logProxyCall('Token refresh successful');
164164
return response.data;
165165
} catch (error) {
166-
logProxyCall('Token refresh failed', { error: (error as Error).message });
166+
this.logDetailedError('Refresh token', error);
167167
throw error;
168168
}
169169
}
@@ -191,7 +191,7 @@ export class IotApiService {
191191
logProxyCall('User data fetched successfully', { userId });
192192
return response.data;
193193
} catch (error) {
194-
logProxyCall('User data fetch failed', { error: (error as Error).message });
194+
this.logDetailedError('Fetch user data', error);
195195
throw error;
196196
}
197197
}
@@ -221,7 +221,7 @@ export class IotApiService {
221221
logProxyCall('Devices listed successfully', { count: response.data?.length || 0 });
222222
return response.data;
223223
} catch (error) {
224-
logProxyCall('List devices failed', { error: (error as Error).message });
224+
this.logDetailedError('List devices', error);
225225
throw error;
226226
}
227227
}
@@ -248,7 +248,7 @@ export class IotApiService {
248248
logProxyCall('Locations listed successfully', { count: response.data?.length || 0 });
249249
return response.data;
250250
} catch (error) {
251-
logProxyCall('List locations failed', { error: (error as Error).message });
251+
this.logDetailedError('List locations', error);
252252
throw error;
253253
}
254254
}
@@ -278,7 +278,7 @@ export class IotApiService {
278278
logProxyCall('Groups listed successfully', { count: response.data?.length || 0 });
279279
return response.data;
280280
} catch (error) {
281-
logProxyCall('List groups failed', { error: (error as Error).message });
281+
this.logDetailedError('List groups', error);
282282
throw error;
283283
}
284284
}
@@ -306,7 +306,7 @@ export class IotApiService {
306306
logProxyCall('Device fetched successfully', { uuid });
307307
return response.data;
308308
} catch (error) {
309-
logProxyCall('Get device failed', { error: (error as Error).message });
309+
this.logDetailedError('Get device', error);
310310
throw error;
311311
}
312312
}
@@ -334,7 +334,7 @@ export class IotApiService {
334334
logProxyCall('Location fetched successfully', { uuid });
335335
return response.data;
336336
} catch (error) {
337-
logProxyCall('Get location failed', { error: (error as Error).message });
337+
this.logDetailedError('Get location', error);
338338
throw error;
339339
}
340340
}
@@ -362,7 +362,7 @@ export class IotApiService {
362362
logProxyCall('Group fetched successfully', { uuid });
363363
return response.data;
364364
} catch (error) {
365-
logProxyCall('Get group failed', { error: (error as Error).message });
365+
this.logDetailedError('Get group', error);
366366
throw error;
367367
}
368368
}
@@ -403,7 +403,7 @@ export class IotApiService {
403403
logProxyCall('Device updated successfully', { uuid });
404404
return response.data;
405405
} catch (error) {
406-
logProxyCall('Update device failed', { error: (error as Error).message });
406+
this.logDetailedError('Update device', error);
407407
throw error;
408408
}
409409
}
@@ -434,7 +434,7 @@ export class IotApiService {
434434
logProxyCall('Device deleted successfully', { uuid });
435435
return response.data;
436436
} catch (error) {
437-
logProxyCall('Delete device failed', { error: (error as Error).message });
437+
this.logDetailedError('Delete device', error);
438438
throw error;
439439
}
440440
}
@@ -459,7 +459,7 @@ export class IotApiService {
459459
logProxyCall('Device state fetched successfully', { deviceUuid });
460460
return response.data;
461461
} catch (error) {
462-
logProxyCall('Get device state failed', { error: (error as Error).message });
462+
this.logDetailedError('Get device state', error);
463463
throw error;
464464
}
465465
}
@@ -484,7 +484,7 @@ export class IotApiService {
484484
logProxyCall('Location state fetched successfully', { locationUuid });
485485
return response.data;
486486
} catch (error) {
487-
logProxyCall('Get location state failed', { error: (error as Error).message });
487+
this.logDetailedError('Get location state', error);
488488
throw error;
489489
}
490490
}
@@ -514,7 +514,7 @@ export class IotApiService {
514514
logProxyCall('Device state by MAC fetched successfully', { macAddress });
515515
return response.data;
516516
} catch (error) {
517-
logProxyCall('Get device state by MAC failed', { error: (error as Error).message });
517+
this.logDetailedError('Get device state by MAC', error);
518518
throw error;
519519
}
520520
}
@@ -550,8 +550,18 @@ export class IotApiService {
550550
logProxyCall('Device control command sent successfully', { eid: controlPayload.eid });
551551
return response.data;
552552
} catch (error) {
553-
logProxyCall('Control device failed', { error: (error as Error).message });
553+
this.logDetailedError('Control device', error);
554554
throw error;
555555
}
556556
}
557+
558+
private logDetailedError(context: string, error: any) {
559+
const errorDetails = {
560+
message: error?.message || 'Unknown error',
561+
status: error?.response?.status || 'N/A',
562+
data: error?.response?.data || 'No response data',
563+
stack: error?.stack || 'No stack trace',
564+
};
565+
logProxyCall(`${context} failed`, errorDetails);
566+
}
557567
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Control Guide Resource Definition
3+
* Explains how to structure device control commands
4+
*/
5+
6+
import * as fs from 'fs';
7+
import * as path from 'path';
8+
9+
/**
10+
* control_guide MCP Resource Definition
11+
*/
12+
export const CONTROL_GUIDE_RESOURCE = {
13+
uri: 'rogo://docs/control-guide',
14+
name: 'Device Control Guide',
15+
description:
16+
'Comprehensive guide on how to control devices: command structure, elements, required fields, and best practices.',
17+
mimeType: 'text/markdown',
18+
19+
async read(): Promise<string> {
20+
try {
21+
const guidePath = path.join(__dirname, '../../../docs/new-tools/how-to-control-devices.md');
22+
return fs.readFileSync(guidePath, 'utf-8');
23+
} catch (error) {
24+
return `# Error Loading Control Guide\n\n${error.message}`;
25+
}
26+
},
27+
};
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Device Attributes Resource Definition
3+
* Provides reference documentation for device attributes, commands, and values
4+
*/
5+
6+
import * as fs from 'fs';
7+
import * as path from 'path';
8+
9+
/**
10+
* device_attributes MCP Resource Definition
11+
*
12+
* Converts device-attr-and-control.csv to markdown format for AI consumption
13+
*/
14+
export const DEVICE_ATTRIBUTES_RESOURCE = {
15+
uri: 'rogo://docs/device-attributes',
16+
name: 'Device Attributes Reference',
17+
description:
18+
'Complete reference of device types, their attributes, valid commands, and value ranges. Use this to understand what commands to send when controlling devices.',
19+
mimeType: 'text/markdown',
20+
21+
/**
22+
* Read and format device attributes documentation
23+
*/
24+
async read(): Promise<string> {
25+
try {
26+
const csvPath = path.join(__dirname, '../../../docs/new-tools/device-attr-and-control.csv');
27+
const csvContent = fs.readFileSync(csvPath, 'utf-8');
28+
29+
// Convert CSV to readable markdown
30+
const lines = csvContent.trim().split('\n').slice(3); // Skip header rows
31+
32+
let markdown = '# Device Attributes Reference\n\n';
33+
markdown +=
34+
'This reference shows what attributes and commands are available for each device type.\n\n';
35+
markdown += '**Command Format**: `[attributeId, value, ...]`\n\n';
36+
markdown += '---\n\n';
37+
38+
let currentDevice = '';
39+
40+
for (const line of lines) {
41+
const parts = line.split(',');
42+
43+
const deviceType = parts[0]?.trim();
44+
const attribute = parts[1]?.trim();
45+
const valueDesc = parts[2]?.trim().replace(/"/g, '');
46+
const example = parts[3]?.trim().replace(/"/g, '');
47+
48+
// New device type
49+
if (deviceType && deviceType !== currentDevice) {
50+
currentDevice = deviceType;
51+
markdown += `## ${deviceType}\n\n`;
52+
}
53+
54+
// Attribute info
55+
if (attribute) {
56+
markdown += `### ${attribute}\n\n`;
57+
markdown += `**Values**: ${valueDesc}\n\n`;
58+
markdown += `**Example**: \`${example}\`\n\n`;
59+
}
60+
}
61+
62+
markdown += '\n---\n\n';
63+
markdown += '## Common Patterns\n\n';
64+
markdown += '- **Turn device ON**: `[1, 1]`\n';
65+
markdown += '- **Turn device OFF**: `[1, 0]`\n';
66+
markdown += '- **Set brightness**: `[28, <value 0-1000>]`\n';
67+
markdown += '- **Set color temperature**: `[29, <value 0-65000>]`\n';
68+
markdown += '- **Set AC temperature**: `[20, <value 15-30>]`\n';
69+
markdown += '- **Set AC mode**: `[17, <0=AUTO|1=COOLING|2=DRY|3=HEATING|4=FAN>]`\n';
70+
71+
return markdown;
72+
} catch (error) {
73+
return `# Error Loading Device Attributes\n\n${error.message}`;
74+
}
75+
},
76+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* State Guide Resource Definition
3+
* Explains how to read and interpret device state
4+
*/
5+
6+
import * as fs from 'fs';
7+
import * as path from 'path';
8+
9+
/**
10+
* state_guide MCP Resource Definition
11+
*/
12+
export const STATE_GUIDE_RESOURCE = {
13+
uri: 'rogo://docs/state-guide',
14+
name: 'Device State Guide',
15+
description:
16+
'How to read and interpret device state structure: state[deviceId][elementId][attributeId] format and attribute values.',
17+
mimeType: 'text/markdown',
18+
19+
async read(): Promise<string> {
20+
try {
21+
const guidePath = path.join(__dirname, '../../../docs/new-tools/how-to-read-state.md');
22+
return fs.readFileSync(guidePath, 'utf-8');
23+
} catch (error) {
24+
return `# Error Loading State Guide\n\n${error.message}`;
25+
}
26+
},
27+
};

src/resources/resources.module.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from '@nestjs/common';
2+
import { ResourceRegistryService } from './services/resource-registry.service';
3+
4+
/**
5+
* ResourcesModule
6+
* Provides MCP resources (documentation, reference materials) for AI consumption
7+
*/
8+
@Module({
9+
providers: [ResourceRegistryService],
10+
exports: [ResourceRegistryService],
11+
})
12+
export class ResourcesModule {}

0 commit comments

Comments
 (0)