@ampeco/public-api-mcp 0.2.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/LICENSE +21 -0
- package/README.md +734 -0
- package/dist/build/flattener.d.ts +39 -0
- package/dist/build/flattener.d.ts.map +1 -0
- package/dist/build/flattener.js +193 -0
- package/dist/build/flattener.js.map +1 -0
- package/dist/build/index.d.ts +13 -0
- package/dist/build/index.d.ts.map +1 -0
- package/dist/build/index.js +175 -0
- package/dist/build/index.js.map +1 -0
- package/dist/build/optimizer.d.ts +32 -0
- package/dist/build/optimizer.d.ts.map +1 -0
- package/dist/build/optimizer.js +222 -0
- package/dist/build/optimizer.js.map +1 -0
- package/dist/build/parser.d.ts +35 -0
- package/dist/build/parser.d.ts.map +1 -0
- package/dist/build/parser.js +83 -0
- package/dist/build/parser.js.map +1 -0
- package/dist/build/types.d.ts +91 -0
- package/dist/build/types.d.ts.map +1 -0
- package/dist/build/types.js +5 -0
- package/dist/build/types.js.map +1 -0
- package/dist/build/zod-generator.d.ts +34 -0
- package/dist/build/zod-generator.d.ts.map +1 -0
- package/dist/build/zod-generator.js +317 -0
- package/dist/build/zod-generator.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +99 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/generated/build-stats.json +7 -0
- package/dist/generated/endpoints.json +32181 -0
- package/dist/generated/schemas.d.ts +8157 -0
- package/dist/generated/schemas.d.ts.map +1 -0
- package/dist/generated/schemas.js +2726 -0
- package/dist/generated/schemas.js.map +1 -0
- package/dist/generated/schemas.ts +3171 -0
- package/dist/server/factory.d.ts +11 -0
- package/dist/server/factory.d.ts.map +1 -0
- package/dist/server/factory.js +450 -0
- package/dist/server/factory.js.map +1 -0
- package/dist/server/factory.test.d.ts +5 -0
- package/dist/server/factory.test.d.ts.map +1 -0
- package/dist/server/factory.test.js +77 -0
- package/dist/server/factory.test.js.map +1 -0
- package/dist/server/http.d.ts +10 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +110 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/index.d.ts +8 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1502 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/loader.d.ts +10 -0
- package/dist/server/loader.d.ts.map +1 -0
- package/dist/server/loader.js +19 -0
- package/dist/server/loader.js.map +1 -0
- package/dist/server/prompts.d.ts +6 -0
- package/dist/server/prompts.d.ts.map +1 -0
- package/dist/server/prompts.js +462 -0
- package/dist/server/prompts.js.map +1 -0
- package/dist/server/stdio.d.ts +10 -0
- package/dist/server/stdio.d.ts.map +1 -0
- package/dist/server/stdio.js +25 -0
- package/dist/server/stdio.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1,1502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server for OpenAPI-defined APIs
|
|
3
|
+
*
|
|
4
|
+
* This server exposes pre-processed OpenAPI endpoints as MCP resources and provides
|
|
5
|
+
* a tool for making authenticated HTTP requests to any endpoint.
|
|
6
|
+
*/
|
|
7
|
+
import express from 'express';
|
|
8
|
+
import cors from 'cors';
|
|
9
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
11
|
+
import { ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { readFileSync } from 'fs';
|
|
14
|
+
import { join, dirname } from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
import fetch from 'node-fetch';
|
|
17
|
+
// Get the directory name for ES modules
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
/**
|
|
21
|
+
* Load pre-processed endpoints from build artifacts
|
|
22
|
+
*/
|
|
23
|
+
function loadEndpoints() {
|
|
24
|
+
const endpointsPath = join(__dirname, '../generated/endpoints.json');
|
|
25
|
+
const content = readFileSync(endpointsPath, 'utf-8');
|
|
26
|
+
return JSON.parse(content);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create and configure the MCP server
|
|
30
|
+
*/
|
|
31
|
+
function createServer(endpoints, bearerToken, hostname) {
|
|
32
|
+
const server = new McpServer({
|
|
33
|
+
name: 'public-api-mcp',
|
|
34
|
+
version: '0.1.0',
|
|
35
|
+
}, {
|
|
36
|
+
capabilities: {
|
|
37
|
+
resources: {},
|
|
38
|
+
tools: {},
|
|
39
|
+
prompts: {},
|
|
40
|
+
logging: {},
|
|
41
|
+
},
|
|
42
|
+
instructions: `This server provides access to the ${endpoints.info.title} API (v${endpoints.info.version}).
|
|
43
|
+
|
|
44
|
+
It exposes ${endpoints.endpoints.filter(e => !e.deprecated).length} active API endpoints through a hierarchical resource structure (deprecated endpoints are automatically filtered out) and provides a tool for making authenticated HTTP requests.
|
|
45
|
+
|
|
46
|
+
Target API: ${hostname}
|
|
47
|
+
|
|
48
|
+
Resource Structure (3-tier hierarchy):
|
|
49
|
+
- Tier 1 (Discovery): List resources returns API tags (e.g., tag://Users, tag://ChargingSessions)
|
|
50
|
+
- Tier 2 (Navigation): Read a tag resource to see all endpoints within that tag
|
|
51
|
+
- Tier 3 (Details): Read an individual endpoint resource for full specification
|
|
52
|
+
|
|
53
|
+
Usage:
|
|
54
|
+
1. List available resources to discover API tags (returns ~10-30 tags instead of 443 endpoints)
|
|
55
|
+
2. Read a tag resource (e.g., tag://Users) to see all endpoints within that tag
|
|
56
|
+
3. Read an endpoint resource (e.g., api://GET/users/{id}) to get detailed endpoint information
|
|
57
|
+
4. Use the 'api_request' tool to make HTTP calls to any endpoint
|
|
58
|
+
|
|
59
|
+
URI Formats:
|
|
60
|
+
- Tags: tag://{TagName} (e.g., tag://Users)
|
|
61
|
+
- Endpoints: api://{METHOD}{path} (e.g., api://GET/users/{id})
|
|
62
|
+
|
|
63
|
+
The server automatically proxies your Bearer token to all API requests to ${hostname}.
|
|
64
|
+
|
|
65
|
+
Note: Deprecated endpoints are not exposed as resources and cannot be accessed. This hierarchical structure reduces initial context consumption by ~95-97%.
|
|
66
|
+
|
|
67
|
+
## Available Prompts
|
|
68
|
+
|
|
69
|
+
This server provides helpful prompts for common workflows. Use these prompts to learn best practices:
|
|
70
|
+
|
|
71
|
+
- **charging_session_workflow**: Learn how to implement the complete charging session lifecycle: finding charge points, starting/stopping sessions, and retrieving session details
|
|
72
|
+
- **authentication_workflow**: Understand how authentication works with the AMPECO API and how to implement custom authentication in your backend
|
|
73
|
+
- **reservation_workflow**: Learn how to reserve charge points for immediate or future use
|
|
74
|
+
- **smart_charging_setup**: Guide for configuring smart charging modes including scheduled, solar, and dynamic pricing strategies
|
|
75
|
+
|
|
76
|
+
To use a prompt, simply reference it by name when you need guidance on these workflows.
|
|
77
|
+
|
|
78
|
+
## AMPECO API Resource Relationships
|
|
79
|
+
|
|
80
|
+
The AMPECO API has a hierarchical structure: **Locations** → **Charging Zones** → **Charge Points** → **EVSEs** → **Connectors**
|
|
81
|
+
|
|
82
|
+
Each resource can be listed independently. EVSEs contain the currentType field ("ac"/"dc") for determining charging type.
|
|
83
|
+
|
|
84
|
+
## Best Practices for Data Fetching
|
|
85
|
+
|
|
86
|
+
**Always prefer top-level listing endpoints over nested fetching:**
|
|
87
|
+
- ✅ Use \`GET /evses\` to get all EVSEs across all charge points (1 API call)
|
|
88
|
+
- ❌ Avoid fetching \`GET /charge-points/{id}/evses\` for each charge point (N API calls)
|
|
89
|
+
|
|
90
|
+
The top-level listing endpoints are more efficient and reduce API load. Use nested endpoints only when you specifically need resources for a single parent entity.
|
|
91
|
+
|
|
92
|
+
## Session State & Webhooks
|
|
93
|
+
|
|
94
|
+
The AMPECO API is event-driven and uses webhooks for real-time updates. When performing actions like starting/stopping charging sessions:
|
|
95
|
+
- Initial API responses return immediately with session IDs
|
|
96
|
+
- Actual state changes arrive via webhooks (SessionUpdateNotification, SessionMeterValuesNotification)
|
|
97
|
+
- Clients should implement webhook handlers to track session status changes from "pending" → "active" → "finished"
|
|
98
|
+
- Session details and final metrics are retrieved via GET requests after receiving webhook notifications
|
|
99
|
+
|
|
100
|
+
## Cursor Pagination Guide
|
|
101
|
+
|
|
102
|
+
Many list endpoints support cursor-based pagination, which is faster and more reliable than offset-based pagination. Here's how to use it:
|
|
103
|
+
|
|
104
|
+
**Initial Request** - Start with an empty cursor:
|
|
105
|
+
\`\`\`
|
|
106
|
+
api_request(endpoint: "/users", method: "GET", query_params: { limit: 100, cursor: "" })
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
**Subsequent Requests** - Use the cursor from the previous response:
|
|
110
|
+
\`\`\`
|
|
111
|
+
api_request(endpoint: "/users", method: "GET", query_params: { limit: 100, cursor: "eyJpZCI6MTAwfQ==" })
|
|
112
|
+
\`\`\`
|
|
113
|
+
|
|
114
|
+
**Pagination Loop:**
|
|
115
|
+
1. Start with \`cursor=""\` (empty string)
|
|
116
|
+
2. Make request with current cursor
|
|
117
|
+
3. Collect items from response
|
|
118
|
+
4. If response contains a non-empty cursor, use it for next request
|
|
119
|
+
5. Stop when cursor is null/empty or data array is empty
|
|
120
|
+
|
|
121
|
+
**Key Points:**
|
|
122
|
+
- ✅ Always use \`cursor=""\` for first request
|
|
123
|
+
- ✅ Use exact cursor value from responses
|
|
124
|
+
- ✅ Check for null/empty cursor or empty data array to detect end
|
|
125
|
+
- ✅ Adjust limit based on data size (100-1000 typical)
|
|
126
|
+
- ❌ Don't use offset pagination when cursor is available`,
|
|
127
|
+
});
|
|
128
|
+
// Use the underlying Server API for dynamic resource handling with many endpoints
|
|
129
|
+
// This gives us more control than the higher-level .resource() method
|
|
130
|
+
// Register resources/list handler - returns tags (Tier 1)
|
|
131
|
+
server.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
132
|
+
const startTime = Date.now();
|
|
133
|
+
console.log(`[Resources List] Building tag-based resource list...`);
|
|
134
|
+
// Filter out deprecated endpoints first
|
|
135
|
+
const activeEndpoints = endpoints.endpoints.filter(endpoint => !endpoint.deprecated);
|
|
136
|
+
// Extract unique tags from all active endpoints
|
|
137
|
+
const tagsSet = new Set();
|
|
138
|
+
let untaggedCount = 0;
|
|
139
|
+
for (const endpoint of activeEndpoints) {
|
|
140
|
+
if (endpoint.tags && endpoint.tags.length > 0) {
|
|
141
|
+
endpoint.tags.forEach(tag => tagsSet.add(tag));
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
untaggedCount++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Convert to sorted array
|
|
148
|
+
const tags = Array.from(tagsSet).sort();
|
|
149
|
+
// Add "Untagged" category if there are untagged endpoints
|
|
150
|
+
if (untaggedCount > 0) {
|
|
151
|
+
tags.push('Untagged');
|
|
152
|
+
}
|
|
153
|
+
// Build resources list with tag URIs
|
|
154
|
+
const resources = tags.map(tag => {
|
|
155
|
+
const tagEndpoints = activeEndpoints.filter(e => tag === 'Untagged'
|
|
156
|
+
? !e.tags || e.tags.length === 0
|
|
157
|
+
: e.tags?.includes(tag));
|
|
158
|
+
return {
|
|
159
|
+
uri: `tag://${tag}`,
|
|
160
|
+
name: tag,
|
|
161
|
+
description: `${tagEndpoints.length} endpoint${tagEndpoints.length !== 1 ? 's' : ''} tagged with '${tag}'`,
|
|
162
|
+
mimeType: 'application/json',
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
const duration = Date.now() - startTime;
|
|
166
|
+
console.log(`[Resources List] Built ${resources.length} tag resources (filtered ${endpoints.endpoints.length - activeEndpoints.length} deprecated endpoints) in ${duration}ms`);
|
|
167
|
+
return { resources };
|
|
168
|
+
});
|
|
169
|
+
// Register resources/read handler - supports tag:// (Tier 2) and api:// (Tier 3) URIs
|
|
170
|
+
server.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
171
|
+
const uri = request.params.uri;
|
|
172
|
+
// Manual URI parsing to handle spaces and special characters in tag names
|
|
173
|
+
// Format: tag://{TagName} or api://{METHOD}{path}
|
|
174
|
+
const protocolMatch = uri.match(/^([^:]+):\/\/(.*)$/);
|
|
175
|
+
if (!protocolMatch) {
|
|
176
|
+
throw new Error(`Invalid URI format: ${uri}`);
|
|
177
|
+
}
|
|
178
|
+
const protocol = protocolMatch[1];
|
|
179
|
+
const remainder = protocolMatch[2];
|
|
180
|
+
// Tier 2: Handle tag:// URIs - return list of endpoints within the tag
|
|
181
|
+
if (protocol === 'tag') {
|
|
182
|
+
const tag = remainder;
|
|
183
|
+
// Filter active endpoints by tag
|
|
184
|
+
const activeEndpoints = endpoints.endpoints.filter(endpoint => !endpoint.deprecated);
|
|
185
|
+
const taggedEndpoints = activeEndpoints.filter(e => tag === 'Untagged'
|
|
186
|
+
? !e.tags || e.tags.length === 0
|
|
187
|
+
: e.tags?.includes(tag));
|
|
188
|
+
if (taggedEndpoints.length === 0) {
|
|
189
|
+
throw new Error(`No endpoints found for tag: ${tag}`);
|
|
190
|
+
}
|
|
191
|
+
// Build response with endpoint summaries
|
|
192
|
+
const tagResource = {
|
|
193
|
+
tag,
|
|
194
|
+
count: taggedEndpoints.length,
|
|
195
|
+
endpoints: taggedEndpoints.map(e => ({
|
|
196
|
+
uri: `api://${e.method.toUpperCase()}${e.path}`,
|
|
197
|
+
method: e.method.toUpperCase(),
|
|
198
|
+
path: e.path,
|
|
199
|
+
summary: e.summary || e.operationId || `${e.method} ${e.path}`,
|
|
200
|
+
operationId: e.operationId,
|
|
201
|
+
})),
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
contents: [
|
|
205
|
+
{
|
|
206
|
+
uri,
|
|
207
|
+
mimeType: 'application/json',
|
|
208
|
+
text: JSON.stringify(tagResource, null, 2),
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
// Tier 3: Handle api:// URIs - return individual endpoint details
|
|
214
|
+
if (protocol === 'api') {
|
|
215
|
+
// Parse URI format: api://METHOD/path
|
|
216
|
+
// Example: api://POST/users or api://GET/users/{id}
|
|
217
|
+
const match = remainder.match(/^([A-Z]+)(\/.*)/);
|
|
218
|
+
if (!match) {
|
|
219
|
+
throw new Error(`Invalid API URI format: ${uri}`);
|
|
220
|
+
}
|
|
221
|
+
const method = match[1];
|
|
222
|
+
const path = match[2];
|
|
223
|
+
// Find the matching endpoint
|
|
224
|
+
const endpoint = endpoints.endpoints.find((e) => e.path === path && e.method.toUpperCase() === method.toUpperCase());
|
|
225
|
+
if (!endpoint) {
|
|
226
|
+
throw new Error(`Endpoint not found: ${method} ${path}`);
|
|
227
|
+
}
|
|
228
|
+
// Reject deprecated endpoints
|
|
229
|
+
if (endpoint.deprecated) {
|
|
230
|
+
throw new Error(`Endpoint is deprecated: ${method} ${path}`);
|
|
231
|
+
}
|
|
232
|
+
// Return comprehensive endpoint information
|
|
233
|
+
return {
|
|
234
|
+
contents: [
|
|
235
|
+
{
|
|
236
|
+
uri,
|
|
237
|
+
mimeType: 'application/json',
|
|
238
|
+
text: JSON.stringify(endpoint, null, 2),
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
// Unsupported protocol
|
|
244
|
+
throw new Error(`Unsupported URI protocol: ${protocol}. Expected 'tag' or 'api'`);
|
|
245
|
+
});
|
|
246
|
+
// Register the API request tool
|
|
247
|
+
server.tool('api_request', `Make an authenticated HTTP request to any active API endpoint from the OpenAPI specification. Deprecated endpoints cannot be called and will return an error. Authentication is handled automatically using the Bearer token from the MCP request. The target API hostname is: ${hostname}
|
|
248
|
+
|
|
249
|
+
CRITICAL - NEVER GUESS: You MUST read the endpoint resource first (using api://{METHOD}{path} URI) before calling this tool. DO NOT guess parameter names, request body structure, or query parameters. The endpoint resource contains the complete specification including all required/optional parameters, request body schema, and response format. Guessing will result in API errors.
|
|
250
|
+
|
|
251
|
+
IMPORTANT - Pagination: Always prefer cursor-based pagination when available. For list endpoints that support both offset and cursor pagination, you MUST use cursor pagination by including an empty "cursor" query parameter (cursor="") in your initial request to engage cursor-based pagination. Subsequent requests should use the cursor value returned in the response.`, {
|
|
252
|
+
endpoint: z.string().describe('The API endpoint path (e.g., "/users/{id}")'),
|
|
253
|
+
method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']).describe('HTTP method'),
|
|
254
|
+
path_params: z.preprocess((val) => (val === null || val === undefined || val === '' ? {} : val), z.record(z.any()).default({})).optional().describe('Path parameter values as key-value pairs (optional)'),
|
|
255
|
+
query_params: z.preprocess((val) => (val === null || val === undefined || val === '' ? {} : val), z.record(z.any()).default({})).optional().describe('Query string parameters as key-value pairs (optional)'),
|
|
256
|
+
body: z.preprocess((val) => (val === null || val === undefined || val === '' ? undefined : val), z.any()).optional().describe('Request body for POST/PUT/PATCH requests (optional)'),
|
|
257
|
+
headers: z.preprocess((val) => (val === null || val === undefined || val === '' ? {} : val), z.record(z.string()).default({})).optional().describe('Additional HTTP headers (optional)'),
|
|
258
|
+
}, async (args) => {
|
|
259
|
+
const { endpoint, method, path_params = {}, query_params = {}, body, headers = {}, } = args;
|
|
260
|
+
// Check if endpoint is deprecated
|
|
261
|
+
const endpointDef = endpoints.endpoints.find((e) => e.path === endpoint && e.method.toUpperCase() === method.toUpperCase());
|
|
262
|
+
if (endpointDef?.deprecated) {
|
|
263
|
+
return {
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: 'text',
|
|
267
|
+
text: JSON.stringify({
|
|
268
|
+
error: true,
|
|
269
|
+
message: `Endpoint is deprecated and cannot be called: ${method} ${endpoint}`,
|
|
270
|
+
}, null, 2),
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
isError: true,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
// Construct the full URL
|
|
277
|
+
let url = hostname.replace(/\/$/, '') + endpoint;
|
|
278
|
+
// Replace path parameters
|
|
279
|
+
for (const [key, value] of Object.entries(path_params)) {
|
|
280
|
+
url = url.replace(`{${key}}`, encodeURIComponent(String(value)));
|
|
281
|
+
}
|
|
282
|
+
// Add query parameters
|
|
283
|
+
const queryString = Object.entries(query_params)
|
|
284
|
+
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
|
285
|
+
.join('&');
|
|
286
|
+
if (queryString) {
|
|
287
|
+
url += `?${queryString}`;
|
|
288
|
+
}
|
|
289
|
+
// Prepare headers with proxied Bearer token
|
|
290
|
+
const requestHeaders = {
|
|
291
|
+
'Content-Type': 'application/json',
|
|
292
|
+
...headers,
|
|
293
|
+
};
|
|
294
|
+
// Add Authorization header if bearer token is available
|
|
295
|
+
if (bearerToken) {
|
|
296
|
+
requestHeaders['Authorization'] = `Bearer ${bearerToken}`;
|
|
297
|
+
}
|
|
298
|
+
// Log the request
|
|
299
|
+
console.log(`[API Request] ${method} ${url}`);
|
|
300
|
+
try {
|
|
301
|
+
// Prepare fetch options
|
|
302
|
+
const fetchOptions = {
|
|
303
|
+
method,
|
|
304
|
+
headers: requestHeaders,
|
|
305
|
+
};
|
|
306
|
+
// Only include body for methods that support it (not GET/HEAD)
|
|
307
|
+
if (body && !['GET', 'HEAD'].includes(method.toUpperCase())) {
|
|
308
|
+
fetchOptions.body = JSON.stringify(body);
|
|
309
|
+
}
|
|
310
|
+
// Make the HTTP request
|
|
311
|
+
const response = await fetch(url, fetchOptions);
|
|
312
|
+
// Parse response
|
|
313
|
+
const rawResponseText = await response.text();
|
|
314
|
+
let responseBody;
|
|
315
|
+
try {
|
|
316
|
+
responseBody = rawResponseText ? JSON.parse(rawResponseText) : null;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
responseBody = rawResponseText;
|
|
320
|
+
}
|
|
321
|
+
// Log the response
|
|
322
|
+
console.log(`[API Response] ${response.status} ${response.statusText}`);
|
|
323
|
+
// Prepare structured response with truncation
|
|
324
|
+
const responseObject = {
|
|
325
|
+
status: response.status,
|
|
326
|
+
statusText: response.statusText,
|
|
327
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
328
|
+
body: responseBody,
|
|
329
|
+
};
|
|
330
|
+
let responseText = JSON.stringify(responseObject, null, 2);
|
|
331
|
+
// Truncate if response exceeds 20000 characters (leaving room for metadata)
|
|
332
|
+
const MAX_RESPONSE_LENGTH = 20000;
|
|
333
|
+
let wasTruncated = false;
|
|
334
|
+
if (responseText.length > MAX_RESPONSE_LENGTH) {
|
|
335
|
+
wasTruncated = true;
|
|
336
|
+
const originalLength = responseText.length;
|
|
337
|
+
responseText = responseText.substring(0, MAX_RESPONSE_LENGTH);
|
|
338
|
+
// Try to truncate at a reasonable boundary (end of a line)
|
|
339
|
+
const lastNewlineIndex = responseText.lastIndexOf('\n');
|
|
340
|
+
if (lastNewlineIndex > MAX_RESPONSE_LENGTH * 0.9) {
|
|
341
|
+
responseText = responseText.substring(0, lastNewlineIndex);
|
|
342
|
+
}
|
|
343
|
+
// Add truncation notice
|
|
344
|
+
responseText += `\n\n... [Response truncated - original size: ${originalLength} characters, showing first ${responseText.length} characters]\n\nNote: The response was too large to display in full. This is typically because the API returned detailed error information or a large dataset. Check the status code and the visible portion of the response for key information.`;
|
|
345
|
+
}
|
|
346
|
+
const toolResponse = {
|
|
347
|
+
content: [
|
|
348
|
+
{
|
|
349
|
+
type: 'text',
|
|
350
|
+
text: responseText,
|
|
351
|
+
},
|
|
352
|
+
],
|
|
353
|
+
};
|
|
354
|
+
console.log(`[Tool Response] Returning response with ${responseText.length} characters${wasTruncated ? ' (truncated)' : ''}`);
|
|
355
|
+
// Return structured response
|
|
356
|
+
return toolResponse;
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
360
|
+
console.error(`[API Error] ${errorMessage}`);
|
|
361
|
+
const errorObject = {
|
|
362
|
+
error: true,
|
|
363
|
+
message: errorMessage,
|
|
364
|
+
details: error instanceof Error ? error.stack : undefined,
|
|
365
|
+
};
|
|
366
|
+
let errorText = JSON.stringify(errorObject, null, 2);
|
|
367
|
+
// Truncate error responses too
|
|
368
|
+
const MAX_ERROR_LENGTH = 5000;
|
|
369
|
+
if (errorText.length > MAX_ERROR_LENGTH) {
|
|
370
|
+
errorText = errorText.substring(0, MAX_ERROR_LENGTH) + '\n\n... [Error message truncated]';
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
content: [
|
|
374
|
+
{
|
|
375
|
+
type: 'text',
|
|
376
|
+
text: errorText,
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
isError: true,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
// Register prompts for common workflows
|
|
384
|
+
server.server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
385
|
+
return {
|
|
386
|
+
prompts: [
|
|
387
|
+
{
|
|
388
|
+
name: 'charging_session_workflow',
|
|
389
|
+
description: 'Learn how to implement the complete charging session lifecycle: finding charge points, starting/stopping sessions, and retrieving session details',
|
|
390
|
+
arguments: [
|
|
391
|
+
{
|
|
392
|
+
name: 'evseId',
|
|
393
|
+
description: 'The EVSE network ID to start charging on (optional - if not provided, guidance will be generic)',
|
|
394
|
+
required: false,
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: 'sessionId',
|
|
398
|
+
description: 'The session ID to monitor or stop (optional - if not provided, guidance will be generic)',
|
|
399
|
+
required: false,
|
|
400
|
+
},
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
name: 'authentication_workflow',
|
|
405
|
+
description: 'Understand how authentication works with the AMPECO API and how to implement custom authentication in your backend',
|
|
406
|
+
arguments: [],
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: 'reservation_workflow',
|
|
410
|
+
description: 'Learn how to reserve charge points for immediate or future use',
|
|
411
|
+
arguments: [
|
|
412
|
+
{
|
|
413
|
+
name: 'chargePointId',
|
|
414
|
+
description: 'The charge point ID to reserve (optional)',
|
|
415
|
+
required: false,
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: 'evseNetworkId',
|
|
419
|
+
description: 'The EVSE network ID to reserve (optional)',
|
|
420
|
+
required: false,
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
name: 'userId',
|
|
424
|
+
description: 'The user ID making the reservation (optional)',
|
|
425
|
+
required: false,
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: 'reservationType',
|
|
429
|
+
description: 'Type of reservation: "immediate" or "future" (optional, defaults to immediate)',
|
|
430
|
+
required: false,
|
|
431
|
+
},
|
|
432
|
+
],
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
name: 'smart_charging_setup',
|
|
436
|
+
description: 'Guide for configuring smart charging modes including scheduled, solar, and dynamic pricing strategies',
|
|
437
|
+
arguments: [
|
|
438
|
+
{
|
|
439
|
+
name: 'chargePointId',
|
|
440
|
+
description: 'The charge point ID to configure (optional)',
|
|
441
|
+
required: false,
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
name: 'mode',
|
|
445
|
+
description: 'Smart charging mode to configure: "scheduled", "solar", "octopus_agile", "octopus_go", "nordpool", or "energi_elspot" (optional)',
|
|
446
|
+
required: false,
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
},
|
|
450
|
+
],
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
server.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
454
|
+
const promptName = request.params.name;
|
|
455
|
+
const args = request.params.arguments || {};
|
|
456
|
+
switch (promptName) {
|
|
457
|
+
case 'charging_session_workflow': {
|
|
458
|
+
const evseId = args.evseId;
|
|
459
|
+
const sessionId = args.sessionId;
|
|
460
|
+
let specificGuidance = '';
|
|
461
|
+
if (evseId) {
|
|
462
|
+
specificGuidance += `\n## Specific Task: Work with EVSE ${evseId}\n\nYou have been provided with EVSE ID: ${evseId}\nUse this ID when starting a charging session in Step 2.\n`;
|
|
463
|
+
}
|
|
464
|
+
if (sessionId) {
|
|
465
|
+
specificGuidance += `\n## Specific Task: Monitor/Stop Session ${sessionId}\n\nYou have been provided with session ID: ${sessionId}\nUse this ID to retrieve session details or stop the charging session.\n`;
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
messages: [
|
|
469
|
+
{
|
|
470
|
+
role: 'user',
|
|
471
|
+
content: {
|
|
472
|
+
type: 'text',
|
|
473
|
+
text: `# Charging Session Workflow${specificGuidance}
|
|
474
|
+
|
|
475
|
+
This guide explains how to implement a complete EV charging session using the AMPECO API.
|
|
476
|
+
|
|
477
|
+
## Overview
|
|
478
|
+
|
|
479
|
+
The charging session lifecycle follows this sequence:
|
|
480
|
+
1. **Discovery**: Find available charge points
|
|
481
|
+
2. **Start Session**: Initiate charging
|
|
482
|
+
3. **Monitor**: Track session progress via webhooks
|
|
483
|
+
4. **Stop Session**: End charging
|
|
484
|
+
5. **Retrieve Details**: Get final session metrics and billing
|
|
485
|
+
|
|
486
|
+
## Step 1: Find Available Charge Points
|
|
487
|
+
|
|
488
|
+
Use the top-level listing endpoints to discover charging infrastructure:
|
|
489
|
+
|
|
490
|
+
**Get Locations:**
|
|
491
|
+
\`\`\`
|
|
492
|
+
GET /resources/locations/v1.0
|
|
493
|
+
\`\`\`
|
|
494
|
+
Returns all locations with geographic coordinates and availability status.
|
|
495
|
+
|
|
496
|
+
**Get Charge Points:**
|
|
497
|
+
\`\`\`
|
|
498
|
+
GET /resources/charge-points/v1.0?status=active&accessType=public&networkStatus=available
|
|
499
|
+
\`\`\`
|
|
500
|
+
Filter for active, public, and available charge points.
|
|
501
|
+
|
|
502
|
+
**Get EVSEs (Recommended for charging type):**
|
|
503
|
+
\`\`\`
|
|
504
|
+
GET /resources/evses
|
|
505
|
+
\`\`\`
|
|
506
|
+
EVSEs contain the \`currentType\` field ("ac" or "dc") which tells you the charging type. Each EVSE belongs to a charge point and contains connectors.
|
|
507
|
+
|
|
508
|
+
## Step 2: Start a Charging Session
|
|
509
|
+
|
|
510
|
+
**Endpoint:**
|
|
511
|
+
\`\`\`
|
|
512
|
+
POST /actions/evse/v1.0/{evseId}/start
|
|
513
|
+
\`\`\`
|
|
514
|
+
|
|
515
|
+
**Required Parameters:**
|
|
516
|
+
- \`evseId\`: The EVSE network ID (from Step 1)
|
|
517
|
+
|
|
518
|
+
**Response:**
|
|
519
|
+
\`\`\`json
|
|
520
|
+
{
|
|
521
|
+
"success": true,
|
|
522
|
+
"sessionId": 12345,
|
|
523
|
+
"message": "Session starting..."
|
|
524
|
+
}
|
|
525
|
+
\`\`\`
|
|
526
|
+
|
|
527
|
+
**Important:** The response returns immediately with a session ID, but the session is not yet active.
|
|
528
|
+
|
|
529
|
+
## Step 3: Monitor Session Status
|
|
530
|
+
|
|
531
|
+
**Webhook Notifications:**
|
|
532
|
+
|
|
533
|
+
The AMPECO API is event-driven. After starting a session, you will receive:
|
|
534
|
+
|
|
535
|
+
1. **First Notification - SessionUpdateNotification:**
|
|
536
|
+
- Status: \`"pending"\`
|
|
537
|
+
- Indicates session is being prepared
|
|
538
|
+
|
|
539
|
+
2. **Second Notification - SessionUpdateNotification:**
|
|
540
|
+
- Status: \`"active"\`
|
|
541
|
+
- Charging has started
|
|
542
|
+
|
|
543
|
+
3. **Ongoing - SessionMeterValuesNotification:**
|
|
544
|
+
- Contains real-time energy consumption (in Wh)
|
|
545
|
+
- Provides meter readings during charging
|
|
546
|
+
|
|
547
|
+
**To get current session details:**
|
|
548
|
+
\`\`\`
|
|
549
|
+
GET /resources/sessions/v1.0/{sessionId}
|
|
550
|
+
\`\`\`
|
|
551
|
+
|
|
552
|
+
## Step 4: Stop Charging Session
|
|
553
|
+
|
|
554
|
+
**Endpoint:**
|
|
555
|
+
\`\`\`
|
|
556
|
+
POST /actions/charge-point/v1.0/{chargePointId}/stop/{sessionId}
|
|
557
|
+
\`\`\`
|
|
558
|
+
|
|
559
|
+
**Required Parameters:**
|
|
560
|
+
- \`chargePointId\`: The charge point ID
|
|
561
|
+
- \`sessionId\`: The session ID from Step 2
|
|
562
|
+
|
|
563
|
+
**Response:**
|
|
564
|
+
Returns confirmation that stop request was sent.
|
|
565
|
+
|
|
566
|
+
**Webhook Notification:**
|
|
567
|
+
You will receive a \`SessionUpdateNotification\` with status \`"finished"\` when the session has fully terminated.
|
|
568
|
+
|
|
569
|
+
## Step 5: Retrieve Final Session Details
|
|
570
|
+
|
|
571
|
+
**Endpoint:**
|
|
572
|
+
\`\`\`
|
|
573
|
+
GET /resources/sessions/v1.0/{sessionId}
|
|
574
|
+
\`\`\`
|
|
575
|
+
|
|
576
|
+
**Wait for:** The \`SessionUpdateNotification\` webhook with status \`"finished"\` before retrieving final details.
|
|
577
|
+
|
|
578
|
+
**Response includes:**
|
|
579
|
+
- Total energy consumed (kWh)
|
|
580
|
+
- State of charge percentage
|
|
581
|
+
- Total amount charged (billing)
|
|
582
|
+
- Payment status
|
|
583
|
+
- Charging periods breakdown
|
|
584
|
+
- Price breakdown by tariff components
|
|
585
|
+
- Complete tariff snapshot
|
|
586
|
+
|
|
587
|
+
## Best Practices
|
|
588
|
+
|
|
589
|
+
1. **Use webhooks for state changes** - Don't poll for status updates
|
|
590
|
+
2. **Wait for "finished" status** - Final metrics are only accurate after session completes
|
|
591
|
+
3. **Use top-level EVSE listing** - More efficient than nested queries
|
|
592
|
+
4. **Check currentType** - Verify AC vs DC compatibility before starting session
|
|
593
|
+
5. **Handle webhook delays** - Status changes may take 10-30 seconds
|
|
594
|
+
|
|
595
|
+
## Error Handling
|
|
596
|
+
|
|
597
|
+
- If start request fails, check EVSE availability and network status
|
|
598
|
+
- If session doesn't transition to "active" within 60 seconds, check charge point hardware status
|
|
599
|
+
- If stop request fails, the session may already be finished or the charge point may be offline`,
|
|
600
|
+
},
|
|
601
|
+
},
|
|
602
|
+
],
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
case 'authentication_workflow': {
|
|
606
|
+
// No arguments needed for this workflow - it's purely educational
|
|
607
|
+
return {
|
|
608
|
+
messages: [
|
|
609
|
+
{
|
|
610
|
+
role: 'user',
|
|
611
|
+
content: {
|
|
612
|
+
type: 'text',
|
|
613
|
+
text: `# Authentication Workflow
|
|
614
|
+
|
|
615
|
+
This guide explains how authentication works with the AMPECO Public API and how to implement it in your application.
|
|
616
|
+
|
|
617
|
+
## Important: No Client-Level Authentication
|
|
618
|
+
|
|
619
|
+
**The AMPECO Public API does NOT provide client-level authentication or authorization for mobile apps or end users.**
|
|
620
|
+
|
|
621
|
+
This means:
|
|
622
|
+
- There is no \`/login\` endpoint for end users
|
|
623
|
+
- There is no user session management in the API
|
|
624
|
+
- You cannot directly authenticate mobile app users against the API
|
|
625
|
+
- You MUST implement your own authentication layer
|
|
626
|
+
|
|
627
|
+
## Authentication Architecture
|
|
628
|
+
|
|
629
|
+
\`\`\`
|
|
630
|
+
Mobile App → Custom Backend → AMPECO Public API
|
|
631
|
+
(Your Auth) (Bearer Token)
|
|
632
|
+
\`\`\`
|
|
633
|
+
|
|
634
|
+
### What You Build (Custom Backend)
|
|
635
|
+
1. User registration and login endpoints
|
|
636
|
+
2. Password hashing and validation
|
|
637
|
+
3. Session/JWT token management for your users
|
|
638
|
+
4. User credential storage in your database
|
|
639
|
+
5. Authorization logic (who can access what)
|
|
640
|
+
|
|
641
|
+
### What AMPECO Provides
|
|
642
|
+
- **Admin-level Bearer Token** for your backend to call the API
|
|
643
|
+
- User resource management via \`POST /resources/users\` (create users in AMPECO system)
|
|
644
|
+
- User-scoped data filtering on API endpoints
|
|
645
|
+
|
|
646
|
+
## Implementation Guide
|
|
647
|
+
|
|
648
|
+
### Step 1: User Registration
|
|
649
|
+
|
|
650
|
+
**Your Backend:**
|
|
651
|
+
1. Accept user registration (email, password, name) from mobile app
|
|
652
|
+
2. Hash password and store in your database
|
|
653
|
+
3. Call AMPECO API to create user resource:
|
|
654
|
+
|
|
655
|
+
\`\`\`
|
|
656
|
+
POST /resources/users
|
|
657
|
+
Authorization: Bearer YOUR_ADMIN_TOKEN
|
|
658
|
+
Content-Type: application/json
|
|
659
|
+
|
|
660
|
+
{
|
|
661
|
+
"email": "user@example.com",
|
|
662
|
+
"first_name": "John",
|
|
663
|
+
"last_name": "Doe"
|
|
664
|
+
}
|
|
665
|
+
\`\`\`
|
|
666
|
+
|
|
667
|
+
4. Store AMPECO user ID alongside your user record
|
|
668
|
+
5. Return success to mobile app
|
|
669
|
+
|
|
670
|
+
### Step 2: User Login
|
|
671
|
+
|
|
672
|
+
**Your Backend:**
|
|
673
|
+
1. Accept login credentials (email, password) from mobile app
|
|
674
|
+
2. Validate credentials against your database
|
|
675
|
+
3. Generate your own session token/JWT
|
|
676
|
+
4. Return token to mobile app
|
|
677
|
+
|
|
678
|
+
**Mobile App:**
|
|
679
|
+
- Store your session token
|
|
680
|
+
- Include it in all requests to your backend
|
|
681
|
+
- Your backend validates this token on each request
|
|
682
|
+
|
|
683
|
+
### Step 3: Making API Calls on Behalf of Users
|
|
684
|
+
|
|
685
|
+
**Your Backend:**
|
|
686
|
+
1. Validate user's session token
|
|
687
|
+
2. Retrieve user's AMPECO user ID from your database
|
|
688
|
+
3. Call AMPECO API with admin Bearer token
|
|
689
|
+
4. Use user filters to scope requests:
|
|
690
|
+
|
|
691
|
+
\`\`\`
|
|
692
|
+
GET /resources/sessions/v1.0?userId={ampecoUserId}
|
|
693
|
+
Authorization: Bearer YOUR_ADMIN_TOKEN
|
|
694
|
+
\`\`\`
|
|
695
|
+
|
|
696
|
+
This returns only sessions for that specific user.
|
|
697
|
+
|
|
698
|
+
### Step 4: Securing the Bearer Token
|
|
699
|
+
|
|
700
|
+
**Critical Security Practices:**
|
|
701
|
+
|
|
702
|
+
1. **NEVER expose admin Bearer token to mobile apps**
|
|
703
|
+
- Token stays on your backend only
|
|
704
|
+
- Mobile apps never see or use it
|
|
705
|
+
|
|
706
|
+
2. **Store token securely:**
|
|
707
|
+
- Environment variables (recommended)
|
|
708
|
+
- Secrets management service (AWS Secrets Manager, etc.)
|
|
709
|
+
- Never commit to git
|
|
710
|
+
|
|
711
|
+
3. **Use HTTPS:**
|
|
712
|
+
- All communication between mobile app and your backend
|
|
713
|
+
- All communication between your backend and AMPECO API
|
|
714
|
+
|
|
715
|
+
## User-Scoped API Requests
|
|
716
|
+
|
|
717
|
+
Many AMPECO endpoints support filtering by user:
|
|
718
|
+
|
|
719
|
+
**Sessions:**
|
|
720
|
+
\`\`\`
|
|
721
|
+
GET /resources/sessions/v1.0?userId={id}
|
|
722
|
+
\`\`\`
|
|
723
|
+
|
|
724
|
+
**Charge Points (user's favorites/home chargers):**
|
|
725
|
+
\`\`\`
|
|
726
|
+
GET /resources/charge-points/v1.0?userId={id}
|
|
727
|
+
\`\`\`
|
|
728
|
+
|
|
729
|
+
**Reservations:**
|
|
730
|
+
\`\`\`
|
|
731
|
+
GET /resources/reservations?userId={id}
|
|
732
|
+
\`\`\`
|
|
733
|
+
|
|
734
|
+
Always use these filters to ensure users only see their own data.
|
|
735
|
+
|
|
736
|
+
## Example Flow: Start Charging Session
|
|
737
|
+
|
|
738
|
+
\`\`\`
|
|
739
|
+
1. Mobile app → Your backend: "Start charging at EVSE 123"
|
|
740
|
+
Headers: Authorization: Bearer YOUR_USER_SESSION_TOKEN
|
|
741
|
+
|
|
742
|
+
2. Your backend validates user session token
|
|
743
|
+
|
|
744
|
+
3. Your backend → AMPECO API:
|
|
745
|
+
POST /actions/evse/v1.0/123/start
|
|
746
|
+
Headers: Authorization: Bearer YOUR_ADMIN_TOKEN
|
|
747
|
+
|
|
748
|
+
4. AMPECO API → Your backend: {"sessionId": 456}
|
|
749
|
+
|
|
750
|
+
5. Your backend → Mobile app: {"sessionId": 456}
|
|
751
|
+
\`\`\`
|
|
752
|
+
|
|
753
|
+
## Best Practices
|
|
754
|
+
|
|
755
|
+
1. **Implement rate limiting** - Protect your backend from abuse
|
|
756
|
+
2. **Log API calls** - Track which user triggered which AMPECO API call
|
|
757
|
+
3. **Handle token expiration** - Refresh admin token if needed
|
|
758
|
+
4. **Validate user ownership** - Ensure users can only stop their own sessions, etc.
|
|
759
|
+
5. **Use webhook authentication** - Verify webhooks are actually from AMPECO
|
|
760
|
+
|
|
761
|
+
## Summary
|
|
762
|
+
|
|
763
|
+
- **You build:** User authentication, session management, password security
|
|
764
|
+
- **AMPECO provides:** Admin-level API access, user resource storage, user-scoped filtering
|
|
765
|
+
- **Your backend** acts as a trusted intermediary between untrusted mobile apps and the AMPECO API`,
|
|
766
|
+
},
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
case 'reservation_workflow': {
|
|
772
|
+
const chargePointId = args.chargePointId;
|
|
773
|
+
const evseNetworkId = args.evseNetworkId;
|
|
774
|
+
const userId = args.userId;
|
|
775
|
+
const reservationType = args.reservationType || 'immediate';
|
|
776
|
+
let specificGuidance = '';
|
|
777
|
+
if (chargePointId || evseNetworkId || userId) {
|
|
778
|
+
specificGuidance += `\n## Specific Task: Create ${reservationType === 'future' ? 'Future' : 'Immediate'} Reservation\n\n`;
|
|
779
|
+
if (userId)
|
|
780
|
+
specificGuidance += `User ID: ${userId}\n`;
|
|
781
|
+
if (chargePointId)
|
|
782
|
+
specificGuidance += `Charge Point ID: ${chargePointId}\n`;
|
|
783
|
+
if (evseNetworkId)
|
|
784
|
+
specificGuidance += `EVSE Network ID: ${evseNetworkId}\n`;
|
|
785
|
+
specificGuidance += `\nUse these IDs when creating the reservation in the examples below.\n`;
|
|
786
|
+
}
|
|
787
|
+
return {
|
|
788
|
+
messages: [
|
|
789
|
+
{
|
|
790
|
+
role: 'user',
|
|
791
|
+
content: {
|
|
792
|
+
type: 'text',
|
|
793
|
+
text: `# Reservation Workflow${specificGuidance}
|
|
794
|
+
|
|
795
|
+
This guide explains how to reserve charge points using the AMPECO API for both immediate and future use.
|
|
796
|
+
|
|
797
|
+
## Overview
|
|
798
|
+
|
|
799
|
+
Reservations allow users to guarantee access to a specific charge point or EVSE. AMPECO supports two types:
|
|
800
|
+
1. **Immediate Reservation** ("Reserve Now") - Reserve for use right now
|
|
801
|
+
2. **Future Reservation** - Reserve for a specific date/time in the future
|
|
802
|
+
|
|
803
|
+
## Reservation Endpoint
|
|
804
|
+
|
|
805
|
+
\`\`\`
|
|
806
|
+
POST /actions/charge-point/v1.0/{chargePointId}/reserve/{evseNetworkId}
|
|
807
|
+
\`\`\`
|
|
808
|
+
|
|
809
|
+
**Path Parameters:**
|
|
810
|
+
- \`chargePointId\`: The charge point ID (number)
|
|
811
|
+
- \`evseNetworkId\`: The EVSE network ID (string) - specific connector to reserve
|
|
812
|
+
|
|
813
|
+
**Request Body:**
|
|
814
|
+
\`\`\`json
|
|
815
|
+
{
|
|
816
|
+
"userId": 123,
|
|
817
|
+
"reason": "User requested reservation"
|
|
818
|
+
}
|
|
819
|
+
\`\`\`
|
|
820
|
+
|
|
821
|
+
## Immediate Reservation ("Reserve Now")
|
|
822
|
+
|
|
823
|
+
**Use Case:** User wants to reserve a charge point to use immediately.
|
|
824
|
+
|
|
825
|
+
**Steps:**
|
|
826
|
+
|
|
827
|
+
1. **Find available charge point:**
|
|
828
|
+
\`\`\`
|
|
829
|
+
GET /resources/charge-points/v1.0?networkStatus=available&hardwareStatus=available
|
|
830
|
+
\`\`\`
|
|
831
|
+
|
|
832
|
+
2. **Get EVSE details:**
|
|
833
|
+
\`\`\`
|
|
834
|
+
GET /resources/evses?chargePointId={chargePointId}
|
|
835
|
+
\`\`\`
|
|
836
|
+
Note the \`networkId\` field from the EVSE you want to reserve.
|
|
837
|
+
|
|
838
|
+
3. **Create immediate reservation:**
|
|
839
|
+
\`\`\`
|
|
840
|
+
POST /actions/charge-point/v1.0/{chargePointId}/reserve/{evseNetworkId}
|
|
841
|
+
Content-Type: application/json
|
|
842
|
+
Authorization: Bearer YOUR_TOKEN
|
|
843
|
+
|
|
844
|
+
{
|
|
845
|
+
"userId": 123,
|
|
846
|
+
"reason": "Immediate charging needed"
|
|
847
|
+
}
|
|
848
|
+
\`\`\`
|
|
849
|
+
|
|
850
|
+
**Response:**
|
|
851
|
+
\`\`\`json
|
|
852
|
+
{
|
|
853
|
+
"success": true,
|
|
854
|
+
"reservationId": 456,
|
|
855
|
+
"status": "accepted",
|
|
856
|
+
"message": "Reservation created successfully"
|
|
857
|
+
}
|
|
858
|
+
\`\`\`
|
|
859
|
+
|
|
860
|
+
4. **Start charging session** (user should plug in soon):
|
|
861
|
+
\`\`\`
|
|
862
|
+
POST /actions/evse/v1.0/{evseNetworkId}/start
|
|
863
|
+
\`\`\`
|
|
864
|
+
|
|
865
|
+
**Important:** Immediate reservations typically expire after 10-15 minutes if not used.
|
|
866
|
+
|
|
867
|
+
## Future Reservation
|
|
868
|
+
|
|
869
|
+
**Use Case:** User wants to reserve a charge point for a specific time (e.g., tomorrow at 8 AM).
|
|
870
|
+
|
|
871
|
+
**Steps:**
|
|
872
|
+
|
|
873
|
+
1. **Identify charge point and EVSE** (same as immediate reservation)
|
|
874
|
+
|
|
875
|
+
2. **Create future reservation:**
|
|
876
|
+
\`\`\`
|
|
877
|
+
POST /actions/charge-point/v1.0/{chargePointId}/reserve/{evseNetworkId}
|
|
878
|
+
Content-Type: application/json
|
|
879
|
+
Authorization: Bearer YOUR_TOKEN
|
|
880
|
+
|
|
881
|
+
{
|
|
882
|
+
"userId": 123,
|
|
883
|
+
"reason": "Reserved for tomorrow morning",
|
|
884
|
+
"expiryDate": "2025-10-11T08:00:00Z",
|
|
885
|
+
"reservationStart": "2025-10-11T08:00:00Z"
|
|
886
|
+
}
|
|
887
|
+
\`\`\`
|
|
888
|
+
|
|
889
|
+
**Note:** Check the OpenAPI spec for exact field names - the API may use \`startDateTime\` or similar fields for scheduling.
|
|
890
|
+
|
|
891
|
+
**Response:**
|
|
892
|
+
\`\`\`json
|
|
893
|
+
{
|
|
894
|
+
"success": true,
|
|
895
|
+
"reservationId": 789,
|
|
896
|
+
"status": "scheduled",
|
|
897
|
+
"startTime": "2025-10-11T08:00:00Z",
|
|
898
|
+
"expiryTime": "2025-10-11T09:00:00Z"
|
|
899
|
+
}
|
|
900
|
+
\`\`\`
|
|
901
|
+
|
|
902
|
+
## Managing Reservations
|
|
903
|
+
|
|
904
|
+
### List User's Reservations
|
|
905
|
+
\`\`\`
|
|
906
|
+
GET /resources/reservations?userId={userId}
|
|
907
|
+
\`\`\`
|
|
908
|
+
|
|
909
|
+
Returns all active and past reservations for the user.
|
|
910
|
+
|
|
911
|
+
### Get Reservation Details
|
|
912
|
+
\`\`\`
|
|
913
|
+
GET /resources/reservations/{reservationId}
|
|
914
|
+
\`\`\`
|
|
915
|
+
|
|
916
|
+
### Cancel Reservation
|
|
917
|
+
Check the OpenAPI spec for the cancel endpoint (typically):
|
|
918
|
+
\`\`\`
|
|
919
|
+
DELETE /resources/reservations/{reservationId}
|
|
920
|
+
\`\`\`
|
|
921
|
+
or
|
|
922
|
+
\`\`\`
|
|
923
|
+
POST /actions/charge-point/v1.0/{chargePointId}/cancel-reservation/{reservationId}
|
|
924
|
+
\`\`\`
|
|
925
|
+
|
|
926
|
+
## Reservation States
|
|
927
|
+
|
|
928
|
+
Reservations go through these states:
|
|
929
|
+
- **accepted** - Reservation created successfully
|
|
930
|
+
- **scheduled** - Future reservation waiting for start time
|
|
931
|
+
- **active** - Reservation time window is now active
|
|
932
|
+
- **expired** - Reservation time passed without use
|
|
933
|
+
- **used** - User started charging session with this reservation
|
|
934
|
+
- **cancelled** - Reservation was cancelled
|
|
935
|
+
|
|
936
|
+
## Error Handling
|
|
937
|
+
|
|
938
|
+
### Reservation Fails
|
|
939
|
+
Common reasons:
|
|
940
|
+
- EVSE already reserved by another user
|
|
941
|
+
- Charge point offline or unavailable
|
|
942
|
+
- Invalid user ID
|
|
943
|
+
- Invalid time range (past date, too far in future)
|
|
944
|
+
|
|
945
|
+
**Check response:**
|
|
946
|
+
\`\`\`json
|
|
947
|
+
{
|
|
948
|
+
"success": false,
|
|
949
|
+
"status": "rejected",
|
|
950
|
+
"message": "EVSE already reserved for that time"
|
|
951
|
+
}
|
|
952
|
+
\`\`\`
|
|
953
|
+
|
|
954
|
+
**Recovery steps:**
|
|
955
|
+
1. Try different EVSE at same location
|
|
956
|
+
2. Adjust reservation time window
|
|
957
|
+
3. Find alternative charge point
|
|
958
|
+
|
|
959
|
+
### Reservation Expires
|
|
960
|
+
If user doesn't show up:
|
|
961
|
+
- Immediate reservations expire after 10-15 minutes
|
|
962
|
+
- Future reservations expire after the scheduled time window ends
|
|
963
|
+
- EVSE becomes available for other users
|
|
964
|
+
- User's reservation record shows "expired" status
|
|
965
|
+
|
|
966
|
+
## Best Practices
|
|
967
|
+
|
|
968
|
+
1. **Check availability before reserving** - Use \`networkStatus\` and \`hardwareStatus\` filters
|
|
969
|
+
2. **Provide clear reason** - Helps with debugging and user support
|
|
970
|
+
3. **Set appropriate expiry** - Don't hold EVSEs longer than needed
|
|
971
|
+
4. **Notify users** - Send reminders before reservation time
|
|
972
|
+
5. **Handle expiry gracefully** - Let users know if their reservation expired
|
|
973
|
+
6. **Allow cancellation** - Users should be able to cancel if plans change
|
|
974
|
+
|
|
975
|
+
## Example: Reserve for Scheduled Charging
|
|
976
|
+
|
|
977
|
+
**Scenario:** User wants to charge during cheap electricity hours (2 AM - 6 AM).
|
|
978
|
+
|
|
979
|
+
1. **Create overnight reservation:**
|
|
980
|
+
\`\`\`
|
|
981
|
+
POST /actions/charge-point/v1.0/42/reserve/EVSE-001
|
|
982
|
+
{
|
|
983
|
+
"userId": 123,
|
|
984
|
+
"reason": "Scheduled charging during off-peak hours",
|
|
985
|
+
"reservationStart": "2025-10-11T02:00:00Z",
|
|
986
|
+
"expiryDate": "2025-10-11T06:00:00Z"
|
|
987
|
+
}
|
|
988
|
+
\`\`\`
|
|
989
|
+
|
|
990
|
+
2. **User plugs in before 2 AM** (or at any time if they're at home)
|
|
991
|
+
|
|
992
|
+
3. **Set smart charging profile** (see smart_charging_setup prompt)
|
|
993
|
+
|
|
994
|
+
4. **Charging starts automatically at 2 AM** using the reservation
|
|
995
|
+
|
|
996
|
+
## Integration with Smart Charging
|
|
997
|
+
|
|
998
|
+
Reservations work well with smart charging modes:
|
|
999
|
+
- Reserve the EVSE
|
|
1000
|
+
- Set a scheduled charging profile
|
|
1001
|
+
- Charging starts automatically during the reservation window
|
|
1002
|
+
- Optimizes for electricity price or solar availability`,
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
],
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
case 'smart_charging_setup': {
|
|
1009
|
+
const chargePointId = args.chargePointId;
|
|
1010
|
+
const mode = args.mode;
|
|
1011
|
+
let specificGuidance = '';
|
|
1012
|
+
if (chargePointId || mode) {
|
|
1013
|
+
specificGuidance += `\n## Specific Task: Configure Smart Charging\n\n`;
|
|
1014
|
+
if (chargePointId)
|
|
1015
|
+
specificGuidance += `Charge Point ID: ${chargePointId}\n`;
|
|
1016
|
+
if (mode) {
|
|
1017
|
+
specificGuidance += `Target Mode: ${mode}\n\n`;
|
|
1018
|
+
specificGuidance += `Focus on implementing the "${mode}" smart charging mode in the examples below.\n`;
|
|
1019
|
+
}
|
|
1020
|
+
else if (chargePointId) {
|
|
1021
|
+
specificGuidance += `\nConfigure smart charging for this charge point using the appropriate mode from the options below.\n`;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
messages: [
|
|
1026
|
+
{
|
|
1027
|
+
role: 'user',
|
|
1028
|
+
content: {
|
|
1029
|
+
type: 'text',
|
|
1030
|
+
text: `# Smart Charging Setup${specificGuidance}
|
|
1031
|
+
|
|
1032
|
+
This guide explains how to configure smart charging modes for home chargers using the AMPECO API.
|
|
1033
|
+
|
|
1034
|
+
## Overview
|
|
1035
|
+
|
|
1036
|
+
Smart charging optimizes when and how EVs charge based on various factors:
|
|
1037
|
+
- **Scheduled** - Charge during specific time windows
|
|
1038
|
+
- **Solar** - Charge using excess solar power
|
|
1039
|
+
- **Dynamic Pricing** - Charge when electricity is cheapest
|
|
1040
|
+
- **Grid Optimization** - Respond to grid demand
|
|
1041
|
+
|
|
1042
|
+
AMPECO supports multiple smart charging modes that can be configured per charge point.
|
|
1043
|
+
|
|
1044
|
+
## Available Smart Charging Modes
|
|
1045
|
+
|
|
1046
|
+
### 1. Scheduled (user_controlled_schedule)
|
|
1047
|
+
Time-based charging within defined windows.
|
|
1048
|
+
|
|
1049
|
+
### 2. Solar (solar)
|
|
1050
|
+
Charges using excess solar power from home panels.
|
|
1051
|
+
|
|
1052
|
+
### 3. Octopus Agile (octopus_agile)
|
|
1053
|
+
Dynamic pricing based on Octopus Energy's Agile tariff (UK).
|
|
1054
|
+
|
|
1055
|
+
### 4. Octopus Go (octopus_go)
|
|
1056
|
+
Fixed off-peak window for Octopus Energy's Go tariff (UK).
|
|
1057
|
+
|
|
1058
|
+
### 5. Energi Elspot (energi_elspot)
|
|
1059
|
+
Nordic spot market electricity pricing.
|
|
1060
|
+
|
|
1061
|
+
### 6. Nordpool (nordpool)
|
|
1062
|
+
Nordic power exchange pricing.
|
|
1063
|
+
|
|
1064
|
+
## Step 1: Get Available Smart Charging Modes
|
|
1065
|
+
|
|
1066
|
+
**Endpoint:**
|
|
1067
|
+
\`\`\`
|
|
1068
|
+
GET /resources/charge-points/v2.0/{chargePointId}/available-personal-smart-charging-modes
|
|
1069
|
+
Authorization: Bearer YOUR_TOKEN
|
|
1070
|
+
\`\`\`
|
|
1071
|
+
|
|
1072
|
+
**Response:**
|
|
1073
|
+
\`\`\`json
|
|
1074
|
+
{
|
|
1075
|
+
"modes": [
|
|
1076
|
+
{
|
|
1077
|
+
"name": "Scheduled",
|
|
1078
|
+
"integrationId": "user_controlled_schedule",
|
|
1079
|
+
"type": "user_controlled_schedule",
|
|
1080
|
+
"defaults": {
|
|
1081
|
+
"start_time": "00:00:00",
|
|
1082
|
+
"end_time": "06:00:00",
|
|
1083
|
+
"target_charge": null
|
|
1084
|
+
}
|
|
1085
|
+
},
|
|
1086
|
+
{
|
|
1087
|
+
"name": "Solar",
|
|
1088
|
+
"integrationId": "solar",
|
|
1089
|
+
"type": "solar",
|
|
1090
|
+
"defaults": {
|
|
1091
|
+
"min_charge_kWh": 10,
|
|
1092
|
+
"max_charge_kWh": 50
|
|
1093
|
+
}
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
"name": "Agile-Octopus",
|
|
1097
|
+
"integrationId": "octopus_agile",
|
|
1098
|
+
"type": "octopus_agile",
|
|
1099
|
+
"defaults": {
|
|
1100
|
+
"max_price_threshold": 15.0
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
]
|
|
1104
|
+
}
|
|
1105
|
+
\`\`\`
|
|
1106
|
+
|
|
1107
|
+
**Note:** Available modes depend on tenant configuration.
|
|
1108
|
+
|
|
1109
|
+
## Step 2: Configure Smart Charging Preferences
|
|
1110
|
+
|
|
1111
|
+
**Endpoint:**
|
|
1112
|
+
\`\`\`
|
|
1113
|
+
POST /resources/charge-points/v2.0/{chargePointId}/personal-smart-charging-preferences
|
|
1114
|
+
Authorization: Bearer YOUR_TOKEN
|
|
1115
|
+
Content-Type: application/json
|
|
1116
|
+
\`\`\`
|
|
1117
|
+
|
|
1118
|
+
### Configuration A: Scheduled Charging
|
|
1119
|
+
|
|
1120
|
+
**Use Case:** Charge only during off-peak hours (2 AM - 6 AM).
|
|
1121
|
+
|
|
1122
|
+
**Request Body:**
|
|
1123
|
+
\`\`\`json
|
|
1124
|
+
{
|
|
1125
|
+
"type": "user_controlled_schedule",
|
|
1126
|
+
"enabled": true,
|
|
1127
|
+
"preferences": {
|
|
1128
|
+
"start_time": "02:00:00",
|
|
1129
|
+
"end_time": "06:00:00",
|
|
1130
|
+
"target_charge": null
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
\`\`\`
|
|
1134
|
+
|
|
1135
|
+
**Parameters:**
|
|
1136
|
+
- \`start_time\`: When charging window begins (24h format)
|
|
1137
|
+
- \`end_time\`: When charging window ends (24h format)
|
|
1138
|
+
- \`target_charge\`: Target kWh (null = charge fully during window)
|
|
1139
|
+
|
|
1140
|
+
**Response:**
|
|
1141
|
+
\`\`\`json
|
|
1142
|
+
{
|
|
1143
|
+
"success": true,
|
|
1144
|
+
"preferenceId": 123,
|
|
1145
|
+
"message": "Smart charging preference saved"
|
|
1146
|
+
}
|
|
1147
|
+
\`\`\`
|
|
1148
|
+
|
|
1149
|
+
### Configuration B: Solar Charging
|
|
1150
|
+
|
|
1151
|
+
**Use Case:** Charge using excess solar power, with minimum guaranteed charge.
|
|
1152
|
+
|
|
1153
|
+
**Request Body:**
|
|
1154
|
+
\`\`\`json
|
|
1155
|
+
{
|
|
1156
|
+
"type": "solar",
|
|
1157
|
+
"enabled": true,
|
|
1158
|
+
"preferences": {
|
|
1159
|
+
"min_charge_kWh": 15,
|
|
1160
|
+
"max_charge_kWh": 60,
|
|
1161
|
+
"allow_grid_charging": true,
|
|
1162
|
+
"grid_charging_threshold": 20
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
\`\`\`
|
|
1166
|
+
|
|
1167
|
+
**Parameters:**
|
|
1168
|
+
- \`min_charge_kWh\`: Minimum energy to charge (guaranteed even from grid)
|
|
1169
|
+
- \`max_charge_kWh\`: Maximum energy to charge
|
|
1170
|
+
- \`allow_grid_charging\`: Allow grid if solar insufficient for minimum
|
|
1171
|
+
- \`grid_charging_threshold\`: Battery % below which to use grid
|
|
1172
|
+
|
|
1173
|
+
**How it works:**
|
|
1174
|
+
1. Charges primarily from solar excess
|
|
1175
|
+
2. If solar insufficient and battery < threshold, uses grid to reach minimum
|
|
1176
|
+
3. Stops at maximum kWh
|
|
1177
|
+
|
|
1178
|
+
### Configuration C: Dynamic Pricing (Octopus Agile)
|
|
1179
|
+
|
|
1180
|
+
**Use Case:** Charge when electricity price is below threshold.
|
|
1181
|
+
|
|
1182
|
+
**Request Body:**
|
|
1183
|
+
\`\`\`json
|
|
1184
|
+
{
|
|
1185
|
+
"type": "octopus_agile",
|
|
1186
|
+
"enabled": true,
|
|
1187
|
+
"preferences": {
|
|
1188
|
+
"max_price_threshold": 12.0,
|
|
1189
|
+
"location": "H",
|
|
1190
|
+
"target_charge_kWh": 40
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
\`\`\`
|
|
1194
|
+
|
|
1195
|
+
**Parameters:**
|
|
1196
|
+
- \`max_price_threshold\`: Maximum p/kWh to charge at (pence)
|
|
1197
|
+
- \`location\`: Octopus pricing region code
|
|
1198
|
+
- \`target_charge_kWh\`: How much energy to add
|
|
1199
|
+
|
|
1200
|
+
**How it works:**
|
|
1201
|
+
1. Monitors Octopus Agile half-hourly prices
|
|
1202
|
+
2. Charges when price drops below threshold
|
|
1203
|
+
3. Stops when target kWh reached
|
|
1204
|
+
|
|
1205
|
+
### Configuration D: Octopus Go (Fixed Off-Peak)
|
|
1206
|
+
|
|
1207
|
+
**Use Case:** Charge during Octopus Go's fixed off-peak window (12:30 AM - 4:30 AM).
|
|
1208
|
+
|
|
1209
|
+
**Request Body:**
|
|
1210
|
+
\`\`\`json
|
|
1211
|
+
{
|
|
1212
|
+
"type": "octopus_go",
|
|
1213
|
+
"enabled": true,
|
|
1214
|
+
"preferences": {
|
|
1215
|
+
"start_time": "00:30:00",
|
|
1216
|
+
"end_time": "04:30:00"
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
\`\`\`
|
|
1220
|
+
|
|
1221
|
+
### Configuration E: Nordpool / Energi Elspot
|
|
1222
|
+
|
|
1223
|
+
**Use Case:** Charge during cheapest Nordic spot market hours.
|
|
1224
|
+
|
|
1225
|
+
**Request Body:**
|
|
1226
|
+
\`\`\`json
|
|
1227
|
+
{
|
|
1228
|
+
"type": "nordpool",
|
|
1229
|
+
"enabled": true,
|
|
1230
|
+
"preferences": {
|
|
1231
|
+
"max_price_threshold": 0.50,
|
|
1232
|
+
"price_area": "SE3",
|
|
1233
|
+
"target_charge_kWh": 50
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
\`\`\`
|
|
1237
|
+
|
|
1238
|
+
**Parameters:**
|
|
1239
|
+
- \`max_price_threshold\`: Maximum price in local currency
|
|
1240
|
+
- \`price_area\`: Nordic price area code (SE1-4, NO1-5, DK1-2, FI)
|
|
1241
|
+
- \`target_charge_kWh\`: Energy target
|
|
1242
|
+
|
|
1243
|
+
## Step 3: Get Current Smart Charging Preferences
|
|
1244
|
+
|
|
1245
|
+
**Endpoint:**
|
|
1246
|
+
\`\`\`
|
|
1247
|
+
GET /resources/charge-points/v2.0/{chargePointId}/personal-smart-charging-preferences
|
|
1248
|
+
Authorization: Bearer YOUR_TOKEN
|
|
1249
|
+
\`\`\`
|
|
1250
|
+
|
|
1251
|
+
**Response:**
|
|
1252
|
+
\`\`\`json
|
|
1253
|
+
{
|
|
1254
|
+
"active": true,
|
|
1255
|
+
"type": "user_controlled_schedule",
|
|
1256
|
+
"preferences": {
|
|
1257
|
+
"start_time": "02:00:00",
|
|
1258
|
+
"end_time": "06:00:00",
|
|
1259
|
+
"target_charge": null
|
|
1260
|
+
},
|
|
1261
|
+
"enabled": true
|
|
1262
|
+
}
|
|
1263
|
+
\`\`\`
|
|
1264
|
+
|
|
1265
|
+
## Step 4: Update or Disable Smart Charging
|
|
1266
|
+
|
|
1267
|
+
### Update Preferences
|
|
1268
|
+
Use the same POST endpoint with new values:
|
|
1269
|
+
\`\`\`
|
|
1270
|
+
POST /resources/charge-points/v2.0/{chargePointId}/personal-smart-charging-preferences
|
|
1271
|
+
{
|
|
1272
|
+
"type": "user_controlled_schedule",
|
|
1273
|
+
"enabled": true,
|
|
1274
|
+
"preferences": {
|
|
1275
|
+
"start_time": "01:00:00",
|
|
1276
|
+
"end_time": "05:00:00"
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
\`\`\`
|
|
1280
|
+
|
|
1281
|
+
### Disable Smart Charging
|
|
1282
|
+
Set \`enabled: false\`:
|
|
1283
|
+
\`\`\`json
|
|
1284
|
+
{
|
|
1285
|
+
"type": "user_controlled_schedule",
|
|
1286
|
+
"enabled": false,
|
|
1287
|
+
"preferences": {}
|
|
1288
|
+
}
|
|
1289
|
+
\`\`\`
|
|
1290
|
+
|
|
1291
|
+
Charging will revert to immediate/manual mode.
|
|
1292
|
+
|
|
1293
|
+
## Best Practices
|
|
1294
|
+
|
|
1295
|
+
1. **Check available modes first** - Not all modes enabled for all tenants
|
|
1296
|
+
2. **Validate time windows** - Ensure start < end for scheduled modes
|
|
1297
|
+
3. **Set reasonable thresholds** - Price thresholds should match local market
|
|
1298
|
+
4. **Combine with reservations** - Reserve EVSE + smart charging = guaranteed optimized charging
|
|
1299
|
+
5. **Monitor effectiveness** - Track sessions to see if smart charging is working
|
|
1300
|
+
6. **Fallback strategy** - Set minimum charge to ensure users aren't stranded
|
|
1301
|
+
|
|
1302
|
+
## Common Workflows
|
|
1303
|
+
|
|
1304
|
+
### Workflow 1: Home Charging Optimization
|
|
1305
|
+
|
|
1306
|
+
\`\`\`
|
|
1307
|
+
1. User has home charger and solar panels
|
|
1308
|
+
2. User wants to charge from solar, but guarantee 20 kWh minimum
|
|
1309
|
+
|
|
1310
|
+
Setup:
|
|
1311
|
+
POST /resources/charge-points/v2.0/{id}/personal-smart-charging-preferences
|
|
1312
|
+
{
|
|
1313
|
+
"type": "solar",
|
|
1314
|
+
"enabled": true,
|
|
1315
|
+
"preferences": {
|
|
1316
|
+
"min_charge_kWh": 20,
|
|
1317
|
+
"max_charge_kWh": 60,
|
|
1318
|
+
"allow_grid_charging": true,
|
|
1319
|
+
"grid_charging_threshold": 30
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
\`\`\`
|
|
1323
|
+
|
|
1324
|
+
### Workflow 2: Cheap Night Charging
|
|
1325
|
+
|
|
1326
|
+
\`\`\`
|
|
1327
|
+
1. User has variable electricity pricing
|
|
1328
|
+
2. Wants to charge only when price < $0.10/kWh
|
|
1329
|
+
|
|
1330
|
+
Setup:
|
|
1331
|
+
POST /resources/charge-points/v2.0/{id}/personal-smart-charging-preferences
|
|
1332
|
+
{
|
|
1333
|
+
"type": "nordpool",
|
|
1334
|
+
"enabled": true,
|
|
1335
|
+
"preferences": {
|
|
1336
|
+
"max_price_threshold": 0.10,
|
|
1337
|
+
"price_area": "SE3",
|
|
1338
|
+
"target_charge_kWh": 40
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
\`\`\`
|
|
1342
|
+
|
|
1343
|
+
### Workflow 3: Fixed Schedule for Apartment
|
|
1344
|
+
|
|
1345
|
+
\`\`\`
|
|
1346
|
+
1. User has fixed off-peak hours (midnight - 6 AM)
|
|
1347
|
+
2. Wants to fully charge during this window
|
|
1348
|
+
|
|
1349
|
+
Setup:
|
|
1350
|
+
POST /resources/charge-points/v2.0/{id}/personal-smart-charging-preferences
|
|
1351
|
+
{
|
|
1352
|
+
"type": "user_controlled_schedule",
|
|
1353
|
+
"enabled": true,
|
|
1354
|
+
"preferences": {
|
|
1355
|
+
"start_time": "00:00:00",
|
|
1356
|
+
"end_time": "06:00:00",
|
|
1357
|
+
"target_charge": null
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
\`\`\`
|
|
1361
|
+
|
|
1362
|
+
## Integration with Charging Sessions
|
|
1363
|
+
|
|
1364
|
+
Smart charging preferences apply automatically to charging sessions:
|
|
1365
|
+
|
|
1366
|
+
1. User plugs in EV
|
|
1367
|
+
2. Session starts (or waits for reservation)
|
|
1368
|
+
3. Smart charging controller manages charging based on active preferences
|
|
1369
|
+
4. Charging occurs only during optimal times/conditions
|
|
1370
|
+
5. Session completes when target reached or window closes
|
|
1371
|
+
|
|
1372
|
+
**No additional API calls needed** - preferences persist until changed.
|
|
1373
|
+
|
|
1374
|
+
## Troubleshooting
|
|
1375
|
+
|
|
1376
|
+
**Smart charging not working:**
|
|
1377
|
+
- Verify \`enabled: true\`
|
|
1378
|
+
- Check charge point supports smart charging (v2.0 endpoints)
|
|
1379
|
+
- Ensure preferences are valid (time format, thresholds)
|
|
1380
|
+
- Verify external integrations are configured (Octopus, Nordpool)
|
|
1381
|
+
|
|
1382
|
+
**Charging outside schedule:**
|
|
1383
|
+
- Check for overriding factors (low battery emergency charge)
|
|
1384
|
+
- Verify no other smart mode is active
|
|
1385
|
+
- Check charge point firmware version`,
|
|
1386
|
+
},
|
|
1387
|
+
},
|
|
1388
|
+
],
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
default:
|
|
1392
|
+
throw new Error(`Unknown prompt: ${promptName}`);
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
return server;
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Main server startup
|
|
1399
|
+
*/
|
|
1400
|
+
async function main() {
|
|
1401
|
+
const port = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;
|
|
1402
|
+
console.log('Loading pre-processed endpoints...');
|
|
1403
|
+
const endpoints = loadEndpoints();
|
|
1404
|
+
console.log(`Loaded ${endpoints.endpoints.length} endpoints from ${endpoints.info.title} v${endpoints.info.version}`);
|
|
1405
|
+
console.log(`Optimization: ${endpoints.stats.reductionPercent.toFixed(2)}% size reduction`);
|
|
1406
|
+
// Create Express app
|
|
1407
|
+
const app = express();
|
|
1408
|
+
app.use(express.json());
|
|
1409
|
+
// Configure CORS to allow access from any domain
|
|
1410
|
+
app.use(cors({
|
|
1411
|
+
origin: true, // Allow all origins (more permissive than '*' for credentials)
|
|
1412
|
+
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD'],
|
|
1413
|
+
allowedHeaders: '*', // Allow all headers
|
|
1414
|
+
exposedHeaders: ['*'], // Expose all headers
|
|
1415
|
+
credentials: true, // Allow credentials
|
|
1416
|
+
preflightContinue: false,
|
|
1417
|
+
optionsSuccessStatus: 204,
|
|
1418
|
+
}));
|
|
1419
|
+
// Health check endpoint
|
|
1420
|
+
app.get('/health', (_req, res) => {
|
|
1421
|
+
res.json({
|
|
1422
|
+
status: 'healthy',
|
|
1423
|
+
server: 'public-api-mcp',
|
|
1424
|
+
version: '0.1.0',
|
|
1425
|
+
endpoints: endpoints.endpoints.length,
|
|
1426
|
+
api: {
|
|
1427
|
+
title: endpoints.info.title,
|
|
1428
|
+
version: endpoints.info.version,
|
|
1429
|
+
},
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
1432
|
+
// MCP endpoint - hostname is extracted from the path
|
|
1433
|
+
// Example: POST /api.example.com or POST /api.example.com/http or POST /api.example.com/https
|
|
1434
|
+
app.post('/*', async (req, res) => {
|
|
1435
|
+
// Extract Bearer token from Authorization header
|
|
1436
|
+
const authHeader = req.headers.authorization;
|
|
1437
|
+
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : '';
|
|
1438
|
+
// Extract hostname and optional protocol from path
|
|
1439
|
+
const fullPath = req.params[0]; // Wildcard capture
|
|
1440
|
+
// Check if path ends with /http or /https
|
|
1441
|
+
let protocol = 'https'; // Default to https
|
|
1442
|
+
let hostnameWithoutSuffix = fullPath;
|
|
1443
|
+
if (fullPath.endsWith('/http')) {
|
|
1444
|
+
protocol = 'http';
|
|
1445
|
+
hostnameWithoutSuffix = fullPath.slice(0, -5); // Remove '/http'
|
|
1446
|
+
}
|
|
1447
|
+
else if (fullPath.endsWith('/https')) {
|
|
1448
|
+
protocol = 'https';
|
|
1449
|
+
hostnameWithoutSuffix = fullPath.slice(0, -6); // Remove '/https'
|
|
1450
|
+
}
|
|
1451
|
+
const hostname = `${protocol}://${hostnameWithoutSuffix}`;
|
|
1452
|
+
console.log(`[MCP Request] Target API: ${hostname}, Method: ${req.body?.method || 'unknown'}`);
|
|
1453
|
+
// Create server instance with the bearer token and hostname
|
|
1454
|
+
const server = createServer(endpoints, bearerToken, hostname);
|
|
1455
|
+
try {
|
|
1456
|
+
// Create stateless transport (no session management)
|
|
1457
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1458
|
+
sessionIdGenerator: undefined,
|
|
1459
|
+
});
|
|
1460
|
+
// Connect server to transport
|
|
1461
|
+
await server.connect(transport);
|
|
1462
|
+
// Handle the request
|
|
1463
|
+
await transport.handleRequest(req, res, req.body);
|
|
1464
|
+
// Clean up when request closes
|
|
1465
|
+
res.on('close', () => {
|
|
1466
|
+
transport.close();
|
|
1467
|
+
server.close();
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
catch (error) {
|
|
1471
|
+
console.error('Error handling MCP request:', error);
|
|
1472
|
+
if (!res.headersSent) {
|
|
1473
|
+
res.status(500).json({
|
|
1474
|
+
jsonrpc: '2.0',
|
|
1475
|
+
error: {
|
|
1476
|
+
code: -32603,
|
|
1477
|
+
message: 'Internal server error',
|
|
1478
|
+
data: error instanceof Error ? error.message : String(error),
|
|
1479
|
+
},
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
// Start server
|
|
1485
|
+
app.listen(port, () => {
|
|
1486
|
+
console.log(`\n🚀 MCP Server started successfully!`);
|
|
1487
|
+
console.log(` Port: ${port}`);
|
|
1488
|
+
console.log(` Health: http://localhost:${port}/health`);
|
|
1489
|
+
console.log(` MCP Endpoint Pattern: http://localhost:${port}/{hostname}[/{protocol}]`);
|
|
1490
|
+
console.log(` Examples:`);
|
|
1491
|
+
console.log(` - http://localhost:${port}/api.example.com (defaults to https)`);
|
|
1492
|
+
console.log(` - http://localhost:${port}/api.example.com/https (explicit https)`);
|
|
1493
|
+
console.log(` - http://localhost:${port}/api.example.com/http (explicit http)`);
|
|
1494
|
+
console.log(`\nReady to accept MCP requests.\n`);
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
// Start the server
|
|
1498
|
+
main().catch((error) => {
|
|
1499
|
+
console.error('Failed to start server:', error);
|
|
1500
|
+
process.exit(1);
|
|
1501
|
+
});
|
|
1502
|
+
//# sourceMappingURL=index.js.map
|