jpdcl 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.
- package/.env.example +15 -0
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/catalog.d.ts +141 -0
- package/dist/catalog.js +261 -0
- package/dist/catalog.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +395 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +13 -0
- package/dist/credentials.js +100 -0
- package/dist/credentials.js.map +1 -0
- package/dist/crypto.d.ts +3 -0
- package/dist/crypto.js +21 -0
- package/dist/crypto.js.map +1 -0
- package/dist/dates.d.ts +5 -0
- package/dist/dates.js +20 -0
- package/dist/dates.js.map +1 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +23 -0
- package/dist/errors.js.map +1 -0
- package/dist/http-handler.d.ts +1 -0
- package/dist/http-handler.js +46 -0
- package/dist/http-handler.js.map +1 -0
- package/dist/http.d.ts +9 -0
- package/dist/http.js +43 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/launcher.d.ts +2 -0
- package/dist/launcher.js +11 -0
- package/dist/launcher.js.map +1 -0
- package/dist/ledger-client.d.ts +112 -0
- package/dist/ledger-client.js +221 -0
- package/dist/ledger-client.js.map +1 -0
- package/dist/main-client.d.ts +25 -0
- package/dist/main-client.js +221 -0
- package/dist/main-client.js.map +1 -0
- package/dist/mcp-stdio.d.ts +2 -0
- package/dist/mcp-stdio.js +6 -0
- package/dist/mcp-stdio.js.map +1 -0
- package/dist/mcp.d.ts +8 -0
- package/dist/mcp.js +517 -0
- package/dist/mcp.js.map +1 -0
- package/dist/runtime.d.ts +27 -0
- package/dist/runtime.js +316 -0
- package/dist/runtime.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +221 -0
- package/dist/server.js.map +1 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +30 -0
- package/dist/session.js.map +1 -0
- package/dist/smart-client.d.ts +50 -0
- package/dist/smart-client.js +238 -0
- package/dist/smart-client.js.map +1 -0
- package/dist/tariff.d.ts +121 -0
- package/dist/tariff.js +124 -0
- package/dist/tariff.js.map +1 -0
- package/dist/types.d.ts +51 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/worker.d.ts +5 -0
- package/dist/worker.js +3 -0
- package/dist/worker.js.map +1 -0
- package/openapi.yaml +284 -0
- package/package.json +89 -0
- package/scripts/audit-http.ts +36 -0
- package/scripts/audit-launcher.mjs +28 -0
- package/scripts/audit-ledger-portal.ts +23 -0
- package/scripts/audit-mcp.mjs +140 -0
- package/scripts/audit-smart-portal.mjs +63 -0
- package/scripts/build.mjs +19 -0
- package/scripts/publish-smithery.mjs +47 -0
- package/src/catalog.ts +291 -0
- package/src/cli.ts +397 -0
- package/src/config.ts +15 -0
- package/src/credentials.ts +114 -0
- package/src/crypto.ts +21 -0
- package/src/dates.ts +19 -0
- package/src/errors.ts +26 -0
- package/src/http-handler.ts +48 -0
- package/src/http.ts +49 -0
- package/src/index.ts +13 -0
- package/src/launcher.ts +9 -0
- package/src/ledger-client.ts +258 -0
- package/src/main-client.ts +230 -0
- package/src/mcp-stdio.ts +6 -0
- package/src/mcp.ts +546 -0
- package/src/runtime.ts +329 -0
- package/src/server.ts +218 -0
- package/src/session.ts +29 -0
- package/src/smart-client.ts +249 -0
- package/src/tariff.ts +148 -0
- package/src/toolkit.test.ts +136 -0
- package/src/types.ts +51 -0
- package/src/worker.ts +3 -0
- package/tsconfig.build.json +11 -0
- package/tsconfig.json +16 -0
package/openapi.yaml
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
openapi: 3.1.0
|
|
2
|
+
info:
|
|
3
|
+
title: JPDCL API-only Toolkit
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
description: Local, normalized REST facade over JPDCL billing, Genus smart-meter, and daily import/export ledger APIs.
|
|
6
|
+
servers:
|
|
7
|
+
- url: http://127.0.0.1:8787
|
|
8
|
+
components:
|
|
9
|
+
securitySchemes:
|
|
10
|
+
LocalApiKey:
|
|
11
|
+
type: http
|
|
12
|
+
scheme: bearer
|
|
13
|
+
description: Optional local key configured with JPDCL_API_KEY.
|
|
14
|
+
schemas:
|
|
15
|
+
PortalResponse:
|
|
16
|
+
type: object
|
|
17
|
+
additionalProperties: true
|
|
18
|
+
RawRequest:
|
|
19
|
+
type: object
|
|
20
|
+
properties:
|
|
21
|
+
params: { type: object, additionalProperties: true }
|
|
22
|
+
body: { type: object, additionalProperties: true }
|
|
23
|
+
confirm: { type: boolean, default: false, description: Required for a cataloged account-changing endpoint in addition to JPDCL_ENABLE_MUTATIONS=true }
|
|
24
|
+
TariffEstimate:
|
|
25
|
+
type: object
|
|
26
|
+
additionalProperties: true
|
|
27
|
+
description: Deterministic domestic charge estimate with rate, slab, provenance, and exclusion details.
|
|
28
|
+
EnergyLedger:
|
|
29
|
+
type: object
|
|
30
|
+
additionalProperties: true
|
|
31
|
+
description: Normalized cumulative daily registers, deterministic deltas, freshness, and provisional net-meter billing quantity.
|
|
32
|
+
MeterHealth:
|
|
33
|
+
type: object
|
|
34
|
+
additionalProperties: true
|
|
35
|
+
description: Unified supply, meter-reading, voltage, outage, ledger-freshness, and alarm evidence.
|
|
36
|
+
paths:
|
|
37
|
+
/health:
|
|
38
|
+
get:
|
|
39
|
+
summary: Service health
|
|
40
|
+
responses:
|
|
41
|
+
"200": { description: Healthy }
|
|
42
|
+
/v1/catalog:
|
|
43
|
+
get:
|
|
44
|
+
summary: Complete discovered endpoint catalog
|
|
45
|
+
parameters:
|
|
46
|
+
- { name: portal, in: query, schema: { type: string, enum: [main, smart, ledger] } }
|
|
47
|
+
responses:
|
|
48
|
+
"200": { description: Endpoint definitions }
|
|
49
|
+
/v1/tariff:
|
|
50
|
+
get:
|
|
51
|
+
summary: Encoded official FY 2025-26 domestic tariff and source pages
|
|
52
|
+
security: [{ LocalApiKey: [] }]
|
|
53
|
+
responses:
|
|
54
|
+
"200": { description: Tariff schedule and general charge terms }
|
|
55
|
+
/v1/auth/login:
|
|
56
|
+
post:
|
|
57
|
+
summary: Create and securely save a main-portal API session
|
|
58
|
+
requestBody:
|
|
59
|
+
required: true
|
|
60
|
+
content:
|
|
61
|
+
application/json:
|
|
62
|
+
schema:
|
|
63
|
+
type: object
|
|
64
|
+
required: [loginId, password]
|
|
65
|
+
properties:
|
|
66
|
+
loginId: { type: string }
|
|
67
|
+
password: { type: string, format: password }
|
|
68
|
+
responses:
|
|
69
|
+
"200": { description: Authenticated }
|
|
70
|
+
/v1/me:
|
|
71
|
+
get:
|
|
72
|
+
summary: Saved session status
|
|
73
|
+
security: [{ LocalApiKey: [] }]
|
|
74
|
+
responses:
|
|
75
|
+
"200": { description: Session metadata }
|
|
76
|
+
/v1/snapshot:
|
|
77
|
+
get:
|
|
78
|
+
summary: Preferred normalized factual snapshot across all three data sources
|
|
79
|
+
description: Automatically uses or refreshes API sessions, adds deterministic ledger and tariff calculations, and excludes forecasts, recommendations, and smart tips.
|
|
80
|
+
security: [{ LocalApiKey: [] }]
|
|
81
|
+
parameters:
|
|
82
|
+
- { name: accountId, in: query, schema: { type: string } }
|
|
83
|
+
responses:
|
|
84
|
+
"200": { description: Provenance-labelled observed-only snapshot }
|
|
85
|
+
/v1/accounts/{accountId}:
|
|
86
|
+
get:
|
|
87
|
+
summary: Consumer, bill, tariff, load, and meter summary
|
|
88
|
+
security: [{ LocalApiKey: [] }]
|
|
89
|
+
parameters: [{ name: accountId, in: path, required: true, schema: { type: string } }]
|
|
90
|
+
responses:
|
|
91
|
+
"200": { description: Account data }
|
|
92
|
+
/v1/accounts/{accountId}/digest:
|
|
93
|
+
get:
|
|
94
|
+
summary: Combined account, bills, payments, consumption, and linked-account digest
|
|
95
|
+
security: [{ LocalApiKey: [] }]
|
|
96
|
+
parameters: [{ name: accountId, in: path, required: true, schema: { type: string } }]
|
|
97
|
+
responses:
|
|
98
|
+
"200": { description: Complete digest }
|
|
99
|
+
/v1/accounts/{accountId}/bills:
|
|
100
|
+
get:
|
|
101
|
+
summary: Billing history
|
|
102
|
+
security: [{ LocalApiKey: [] }]
|
|
103
|
+
parameters:
|
|
104
|
+
- { name: accountId, in: path, required: true, schema: { type: string } }
|
|
105
|
+
- { name: from, in: query, schema: { type: string, format: date } }
|
|
106
|
+
- { name: to, in: query, schema: { type: string, format: date } }
|
|
107
|
+
responses:
|
|
108
|
+
"200": { description: Bills }
|
|
109
|
+
/v1/accounts/{accountId}/payments:
|
|
110
|
+
get:
|
|
111
|
+
summary: Payment history
|
|
112
|
+
security: [{ LocalApiKey: [] }]
|
|
113
|
+
parameters:
|
|
114
|
+
- { name: accountId, in: path, required: true, schema: { type: string } }
|
|
115
|
+
- { name: from, in: query, schema: { type: string, format: date } }
|
|
116
|
+
- { name: to, in: query, schema: { type: string, format: date } }
|
|
117
|
+
responses:
|
|
118
|
+
"200": { description: Payments }
|
|
119
|
+
/v1/accounts/{accountId}/consumption:
|
|
120
|
+
get:
|
|
121
|
+
summary: Main-portal billed consumption
|
|
122
|
+
security: [{ LocalApiKey: [] }]
|
|
123
|
+
parameters:
|
|
124
|
+
- { name: accountId, in: path, required: true, schema: { type: string } }
|
|
125
|
+
- { name: from, in: query, schema: { type: string, format: date } }
|
|
126
|
+
- { name: to, in: query, schema: { type: string, format: date } }
|
|
127
|
+
responses:
|
|
128
|
+
"200": { description: Consumption }
|
|
129
|
+
/v1/accounts/{accountId}/tariff-estimate:
|
|
130
|
+
get:
|
|
131
|
+
summary: Automatic domestic electricity charge estimate
|
|
132
|
+
description: Prefers current-month net-import register differences for net meters, with Genus usage as fallback; detects category, plan and sanctioned load. The result is deterministic but is not a utility bill.
|
|
133
|
+
security: [{ LocalApiKey: [] }]
|
|
134
|
+
parameters:
|
|
135
|
+
- { name: accountId, in: path, required: true, schema: { type: string } }
|
|
136
|
+
- { name: units, in: query, schema: { type: number, minimum: 0 }, description: Optional kWh override }
|
|
137
|
+
- { name: load, in: query, schema: { type: number, minimum: 0 }, description: Optional sanctioned-load override }
|
|
138
|
+
- { name: prepaid, in: query, schema: { type: boolean }, description: Optional plan override }
|
|
139
|
+
- { name: solarWaterHeaterEligible, in: query, schema: { type: boolean }, description: Apply only when verified by JPDCL }
|
|
140
|
+
- { name: electricityDuty, in: query, schema: { type: number, minimum: 0 }, description: Explicit duty amount; never inferred }
|
|
141
|
+
- { name: otherCharges, in: query, schema: { type: number, minimum: 0 } }
|
|
142
|
+
- { name: unpaidPrincipal, in: query, schema: { type: number, minimum: 0 } }
|
|
143
|
+
- { name: lateMonths, in: query, schema: { type: number, minimum: 0 } }
|
|
144
|
+
responses:
|
|
145
|
+
"200":
|
|
146
|
+
description: Provenance-labelled tariff estimate
|
|
147
|
+
content:
|
|
148
|
+
application/json:
|
|
149
|
+
schema: { $ref: "#/components/schemas/TariffEstimate" }
|
|
150
|
+
/v1/accounts/{accountId}/energy-ledger:
|
|
151
|
+
get:
|
|
152
|
+
summary: Daily import, export, net-import, and apparent-energy ledger
|
|
153
|
+
description: Resolves the consumer ID from the authenticated account, then returns normalized registers, daily/period deltas, source freshness, and provisional billable kWh.
|
|
154
|
+
security: [{ LocalApiKey: [] }]
|
|
155
|
+
parameters:
|
|
156
|
+
- { name: accountId, in: path, required: true, schema: { type: string } }
|
|
157
|
+
- { name: from, in: query, schema: { type: string, format: date } }
|
|
158
|
+
- { name: to, in: query, schema: { type: string, format: date } }
|
|
159
|
+
- { name: limit, in: query, schema: { type: integer, minimum: 0, maximum: 500, default: 35 }, description: Daily rows; zero returns only the period summary }
|
|
160
|
+
responses:
|
|
161
|
+
"200":
|
|
162
|
+
description: Provenance-labelled energy ledger
|
|
163
|
+
content:
|
|
164
|
+
application/json:
|
|
165
|
+
schema: { $ref: "#/components/schemas/EnergyLedger" }
|
|
166
|
+
/v1/accounts/{accountId}/meter-health:
|
|
167
|
+
get:
|
|
168
|
+
summary: Unified meter, electrical supply, and data-freshness status
|
|
169
|
+
description: Combines the latest utility status, on-demand readings, voltage profile, power events, daily register freshness, and meter alarms. It does not invent a network-connectivity status the portals do not expose.
|
|
170
|
+
security: [{ LocalApiKey: [] }]
|
|
171
|
+
parameters: [{ name: accountId, in: path, required: true, schema: { type: string } }]
|
|
172
|
+
responses:
|
|
173
|
+
"200":
|
|
174
|
+
description: Multi-source meter-health evidence
|
|
175
|
+
content:
|
|
176
|
+
application/json:
|
|
177
|
+
schema: { $ref: "#/components/schemas/MeterHealth" }
|
|
178
|
+
/v1/smart/session:
|
|
179
|
+
get:
|
|
180
|
+
summary: Smart-meter session and account metadata
|
|
181
|
+
security: [{ LocalApiKey: [] }]
|
|
182
|
+
responses:
|
|
183
|
+
"200": { description: Smart account metadata }
|
|
184
|
+
/v1/smart/accounts/{accountId}/dashboard:
|
|
185
|
+
get:
|
|
186
|
+
summary: Measured readings and account records; derived content excluded by default
|
|
187
|
+
security: [{ LocalApiKey: [] }]
|
|
188
|
+
parameters:
|
|
189
|
+
- { name: accountId, in: path, required: true, schema: { type: string } }
|
|
190
|
+
- { name: includeDerived, in: query, schema: { type: boolean, default: false }, description: Opt in to clearly labelled predictions, estimates, insights, and tips }
|
|
191
|
+
responses:
|
|
192
|
+
"200": { description: Live usage, reading, bill, alerts, and session }
|
|
193
|
+
/v1/smart/accounts/{accountId}/meter:
|
|
194
|
+
get:
|
|
195
|
+
summary: Meter technical profile and current reading
|
|
196
|
+
security: [{ LocalApiKey: [] }]
|
|
197
|
+
parameters: [{ name: accountId, in: path, required: true, schema: { type: string } }]
|
|
198
|
+
responses:
|
|
199
|
+
"200": { description: Meter and connection details }
|
|
200
|
+
/v1/smart/accounts/{accountId}/alerts:
|
|
201
|
+
get:
|
|
202
|
+
summary: Live daily and monthly usage alerts
|
|
203
|
+
security: [{ LocalApiKey: [] }]
|
|
204
|
+
parameters: [{ name: accountId, in: path, required: true, schema: { type: string } }]
|
|
205
|
+
responses:
|
|
206
|
+
"200": { description: Alert state and thresholds }
|
|
207
|
+
/v1/smart/accounts/{accountId}/preferences:
|
|
208
|
+
get:
|
|
209
|
+
summary: Notification categories and delivery-channel preferences
|
|
210
|
+
security: [{ LocalApiKey: [] }]
|
|
211
|
+
parameters: [{ name: accountId, in: path, required: true, schema: { type: string } }]
|
|
212
|
+
responses:
|
|
213
|
+
"200": { description: Preferences }
|
|
214
|
+
/v1/smart/accounts/{accountId}/notifications:
|
|
215
|
+
get:
|
|
216
|
+
summary: Notifications and unread count
|
|
217
|
+
security: [{ LocalApiKey: [] }]
|
|
218
|
+
parameters: [{ name: accountId, in: path, required: true, schema: { type: string } }]
|
|
219
|
+
responses:
|
|
220
|
+
"200": { description: Notifications }
|
|
221
|
+
/v1/smart/accounts/{accountId}/support:
|
|
222
|
+
get:
|
|
223
|
+
summary: FAQs, contact details, categories, and complaints
|
|
224
|
+
security: [{ LocalApiKey: [] }]
|
|
225
|
+
parameters:
|
|
226
|
+
- { name: accountId, in: path, required: true, schema: { type: string } }
|
|
227
|
+
- { name: page, in: query, schema: { type: integer, default: 1 } }
|
|
228
|
+
- { name: pageSize, in: query, schema: { type: integer, default: 20 } }
|
|
229
|
+
- { name: statusCodes, in: query, schema: { type: string } }
|
|
230
|
+
responses:
|
|
231
|
+
"200": { description: Support data }
|
|
232
|
+
/v1/smart/accounts/{accountId}/consumption:
|
|
233
|
+
get:
|
|
234
|
+
summary: Smart-meter consumption comparison
|
|
235
|
+
security: [{ LocalApiKey: [] }]
|
|
236
|
+
parameters:
|
|
237
|
+
- { name: accountId, in: path, required: true, schema: { type: string } }
|
|
238
|
+
- { name: type, in: query, schema: { type: string, enum: [daily, weekly, monthly], default: monthly } }
|
|
239
|
+
- { name: value, in: query, schema: { type: integer, default: 12 } }
|
|
240
|
+
responses:
|
|
241
|
+
"200": { description: Consumption comparison }
|
|
242
|
+
/v1/smart/accounts/{accountId}/intervals:
|
|
243
|
+
get:
|
|
244
|
+
summary: Half-hour smart-meter readings
|
|
245
|
+
security: [{ LocalApiKey: [] }]
|
|
246
|
+
parameters:
|
|
247
|
+
- { name: accountId, in: path, required: true, schema: { type: string } }
|
|
248
|
+
- { name: from, in: query, required: true, schema: { type: string, format: date } }
|
|
249
|
+
- { name: to, in: query, required: true, schema: { type: string, format: date } }
|
|
250
|
+
- { name: sort, in: query, schema: { type: string, default: date } }
|
|
251
|
+
responses:
|
|
252
|
+
"200": { description: Interval readings }
|
|
253
|
+
/v1/smart/accounts/{accountId}/reports/{reportType}:
|
|
254
|
+
get:
|
|
255
|
+
summary: Smart-meter analytical report
|
|
256
|
+
security: [{ LocalApiKey: [] }]
|
|
257
|
+
parameters:
|
|
258
|
+
- { name: accountId, in: path, required: true, schema: { type: string } }
|
|
259
|
+
- { name: reportType, in: path, required: true, schema: { type: string } }
|
|
260
|
+
- { name: from, in: query, schema: { type: string, format: date } }
|
|
261
|
+
- { name: to, in: query, schema: { type: string, format: date } }
|
|
262
|
+
- { name: start, in: query, schema: { type: integer, default: 1 } }
|
|
263
|
+
- { name: end, in: query, schema: { type: integer } }
|
|
264
|
+
- { name: filter, in: query, schema: { type: string } }
|
|
265
|
+
- { name: format, in: query, schema: { type: string, enum: [xlsx, pdf] } }
|
|
266
|
+
responses:
|
|
267
|
+
"200": { description: Report data or base64-encoded download }
|
|
268
|
+
/v1/raw/{endpoint}:
|
|
269
|
+
get:
|
|
270
|
+
summary: Call any cataloged read endpoint with query parameters
|
|
271
|
+
security: [{ LocalApiKey: [] }]
|
|
272
|
+
parameters: [{ name: endpoint, in: path, required: true, schema: { type: string } }]
|
|
273
|
+
responses:
|
|
274
|
+
"200": { description: Portal response }
|
|
275
|
+
post:
|
|
276
|
+
summary: Call any cataloged endpoint with explicit path parameters and body
|
|
277
|
+
security: [{ LocalApiKey: [] }]
|
|
278
|
+
parameters: [{ name: endpoint, in: path, required: true, schema: { type: string } }]
|
|
279
|
+
requestBody:
|
|
280
|
+
content:
|
|
281
|
+
application/json:
|
|
282
|
+
schema: { $ref: "#/components/schemas/RawRequest" }
|
|
283
|
+
responses:
|
|
284
|
+
"200": { description: Portal response }
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jpdcl",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Unofficial Jammu Power Distribution Corporation Limited (JPDCL) smart-meter MCP, CLI, REST API, and TypeScript client",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"jpdcl",
|
|
8
|
+
"jammu-power-distribution",
|
|
9
|
+
"jammu-kashmir",
|
|
10
|
+
"jk-electricity",
|
|
11
|
+
"electricity",
|
|
12
|
+
"smart-meter",
|
|
13
|
+
"mcp",
|
|
14
|
+
"model-context-protocol",
|
|
15
|
+
"cli",
|
|
16
|
+
"api"
|
|
17
|
+
],
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/ashfaqmehmood/jpdcl#readme",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/ashfaqmehmood/jpdcl.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/ashfaqmehmood/jpdcl/issues"
|
|
28
|
+
},
|
|
29
|
+
"overrides": {
|
|
30
|
+
"@hono/node-server": "$@hono/node-server"
|
|
31
|
+
},
|
|
32
|
+
"type": "module",
|
|
33
|
+
"bin": {
|
|
34
|
+
"jpdcl": "dist/launcher.js",
|
|
35
|
+
"jpdcl-mcp": "dist/mcp-stdio.js",
|
|
36
|
+
"jpdcl-api": "dist/server.js"
|
|
37
|
+
},
|
|
38
|
+
"exports": {
|
|
39
|
+
".": "./dist/index.js"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"src",
|
|
44
|
+
"scripts",
|
|
45
|
+
"tsconfig.json",
|
|
46
|
+
"tsconfig.build.json",
|
|
47
|
+
"README.md",
|
|
48
|
+
"openapi.yaml",
|
|
49
|
+
".env.example"
|
|
50
|
+
],
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "node scripts/build.mjs",
|
|
53
|
+
"publish:smithery": "node scripts/publish-smithery.mjs",
|
|
54
|
+
"dev:cli": "node --import tsx src/cli.ts",
|
|
55
|
+
"dev:api": "node --import tsx src/server.ts",
|
|
56
|
+
"dev:mcp": "node --import tsx src/mcp.ts",
|
|
57
|
+
"dev": "node --import tsx src/launcher.ts",
|
|
58
|
+
"typecheck": "tsc --noEmit",
|
|
59
|
+
"test": "node --import tsx --test src/toolkit.test.ts",
|
|
60
|
+
"audit:smart": "node --import tsx scripts/audit-smart-portal.mjs",
|
|
61
|
+
"audit:ledger": "node --import tsx scripts/audit-ledger-portal.ts",
|
|
62
|
+
"audit:launcher": "npm run build --silent && node scripts/audit-launcher.mjs",
|
|
63
|
+
"audit:http": "node --import tsx scripts/audit-http.ts",
|
|
64
|
+
"audit:mcp": "npm run build --silent && node scripts/audit-mcp.mjs"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=20"
|
|
68
|
+
},
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"@hono/node-server": "^2.0.11",
|
|
71
|
+
"@inquirer/prompts": "^7.8.0",
|
|
72
|
+
"@modelcontextprotocol/sdk": "^1.17.2",
|
|
73
|
+
"cheerio": "^1.2.0",
|
|
74
|
+
"commander": "^14.0.0",
|
|
75
|
+
"crypto-js": "^4.2.0",
|
|
76
|
+
"dotenv": "^17.4.2",
|
|
77
|
+
"fetch-cookie": "^3.1.0",
|
|
78
|
+
"hono": "^4.8.10",
|
|
79
|
+
"tough-cookie": "^5.1.2",
|
|
80
|
+
"zod": "^4.4.3"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@types/crypto-js": "^4.2.2",
|
|
84
|
+
"@types/node": "^24.1.0",
|
|
85
|
+
"tsx": "^4.20.3",
|
|
86
|
+
"typescript": "^5.8.3",
|
|
87
|
+
"wrangler": "^4.114.0"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
import { handleMcpRequest } from "../src/http-handler.js";
|
|
5
|
+
|
|
6
|
+
const listener = await new Promise<ReturnType<typeof serve>>((resolve) => {
|
|
7
|
+
const server = serve({ fetch: handleMcpRequest, port: 0 }, () => resolve(server));
|
|
8
|
+
});
|
|
9
|
+
const address = listener.address();
|
|
10
|
+
if (!address || typeof address === "string") throw new Error("Could not start HTTP audit server");
|
|
11
|
+
|
|
12
|
+
const transport = new StreamableHTTPClientTransport(new URL(`http://127.0.0.1:${address.port}/api/mcp`), {
|
|
13
|
+
requestInit: {
|
|
14
|
+
headers: {
|
|
15
|
+
"x-jpdcl-login-id": "audit-user",
|
|
16
|
+
"x-jpdcl-password": "audit-password",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
const client = new Client({ name: "jpdcl-http-audit", version: "1.0.0" });
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
await client.connect(transport);
|
|
24
|
+
const tools = await client.listTools();
|
|
25
|
+
const status = await client.callTool({ name: "jpdcl_session_status", arguments: {} });
|
|
26
|
+
if (client.getServerVersion()?.name !== "JPDCL Smart Meter" || tools.tools.length !== 29) {
|
|
27
|
+
throw new Error("HTTP endpoint returned unexpected MCP metadata");
|
|
28
|
+
}
|
|
29
|
+
if (status.isError || status.structuredContent?.credentialSource !== "smithery-connection") {
|
|
30
|
+
throw new Error("HTTP credentials were not isolated as connection configuration");
|
|
31
|
+
}
|
|
32
|
+
process.stdout.write(`${JSON.stringify({ status: "ok", transport: "streamable-http", toolCount: tools.tools.length })}\n`);
|
|
33
|
+
} finally {
|
|
34
|
+
await client.close();
|
|
35
|
+
listener.close();
|
|
36
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
4
|
+
|
|
5
|
+
const launcher = new URL("../dist/launcher.js", import.meta.url).pathname;
|
|
6
|
+
const cli = spawnSync(process.execPath, [launcher, "--help"], { encoding: "utf8" });
|
|
7
|
+
if (cli.status !== 0 || !cli.stdout.includes("Usage: jpdcl")) {
|
|
8
|
+
throw new Error(`jpdcl CLI dispatch failed: ${cli.stderr || cli.stdout}`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const transport = new StdioClientTransport({
|
|
12
|
+
command: process.execPath,
|
|
13
|
+
args: [launcher],
|
|
14
|
+
cwd: new URL("..", import.meta.url).pathname,
|
|
15
|
+
stderr: "pipe",
|
|
16
|
+
});
|
|
17
|
+
const client = new Client({ name: "jpdcl-launcher-audit", version: "1.0.0" });
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await client.connect(transport);
|
|
21
|
+
const tools = await client.listTools();
|
|
22
|
+
if (client.getServerVersion()?.name !== "JPDCL Smart Meter" || tools.tools.length !== 29) {
|
|
23
|
+
throw new Error("jpdcl MCP dispatch returned unexpected server metadata");
|
|
24
|
+
}
|
|
25
|
+
process.stdout.write(`${JSON.stringify({ status: "ok", command: "jpdcl", cli: true, mcp: true, toolCount: tools.tools.length })}\n`);
|
|
26
|
+
} finally {
|
|
27
|
+
await client.close();
|
|
28
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { JpdclRuntime } from "../src/runtime.js";
|
|
2
|
+
|
|
3
|
+
const runtime = await JpdclRuntime.create();
|
|
4
|
+
const ledger = await runtime.energyLedger(undefined, { limit: 1 });
|
|
5
|
+
const consumer = ledger.consumer as { smartMeterNumber?: string | null; netMeter?: boolean | null };
|
|
6
|
+
const availability = ledger.availability as { sourceRecordCount?: number; latestObservation?: string | null };
|
|
7
|
+
const readings = ledger.readings as Array<{ cumulative?: Record<string, unknown> }>;
|
|
8
|
+
const requiredRegisters = ["importKwh", "exportKwh", "netImportKwh", "importKvah", "exportKvah", "netImportKvah"];
|
|
9
|
+
const missing = requiredRegisters.filter((key) => !(key in (readings[0]?.cumulative ?? {})));
|
|
10
|
+
if (!availability.sourceRecordCount || !readings.length || missing.length) {
|
|
11
|
+
throw new Error(`Daily ledger audit failed; missing registers: ${missing.join(", ") || "reading rows"}`);
|
|
12
|
+
}
|
|
13
|
+
if (!consumer.smartMeterNumber) throw new Error("Daily ledger audit failed; meter number is missing");
|
|
14
|
+
const alarms = await runtime.ledger.alarms(consumer.smartMeterNumber);
|
|
15
|
+
|
|
16
|
+
process.stdout.write(`${JSON.stringify({
|
|
17
|
+
status: "ok",
|
|
18
|
+
sourceRecordCount: availability.sourceRecordCount,
|
|
19
|
+
latestObservation: availability.latestObservation,
|
|
20
|
+
netMeter: consumer.netMeter,
|
|
21
|
+
registers: requiredRegisters,
|
|
22
|
+
alarmResponseParsed: typeof alarms.hasAlarms === "boolean",
|
|
23
|
+
}, null, 2)}\n`);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
|
|
4
|
+
const isoDaysAgo = (days) => {
|
|
5
|
+
const date = new Date();
|
|
6
|
+
date.setUTCDate(date.getUTCDate() - days);
|
|
7
|
+
return date.toISOString().slice(0, 10);
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const today = isoDaysAgo(0);
|
|
11
|
+
const recent = isoDaysAgo(7);
|
|
12
|
+
const historyStart = isoDaysAgo(150);
|
|
13
|
+
const cases = {
|
|
14
|
+
jpdcl_catalog: {},
|
|
15
|
+
jpdcl_guide: {},
|
|
16
|
+
jpdcl_session_status: {},
|
|
17
|
+
jpdcl_snapshot: {},
|
|
18
|
+
jpdcl_tariff_estimate: {},
|
|
19
|
+
jpdcl_tariff_schedule: {},
|
|
20
|
+
jpdcl_energy_ledger: { limit: 2 },
|
|
21
|
+
jpdcl_meter_health: {},
|
|
22
|
+
jpdcl_account_info: {},
|
|
23
|
+
jpdcl_account_digest: {},
|
|
24
|
+
jpdcl_bills: { from: historyStart, to: today },
|
|
25
|
+
jpdcl_payments: { from: historyStart, to: today },
|
|
26
|
+
jpdcl_consumption: { from: historyStart, to: today },
|
|
27
|
+
jpdcl_smart_session: {},
|
|
28
|
+
jpdcl_smart_dashboard: {},
|
|
29
|
+
jpdcl_smart_consumption: { type: "monthly", value: 2 },
|
|
30
|
+
jpdcl_smart_intervals: { from: recent, to: today },
|
|
31
|
+
jpdcl_smart_meter_profile: {},
|
|
32
|
+
jpdcl_smart_forecasts: {},
|
|
33
|
+
jpdcl_smart_billing: {},
|
|
34
|
+
jpdcl_smart_alerts: {},
|
|
35
|
+
jpdcl_smart_preferences: {},
|
|
36
|
+
jpdcl_smart_support: { pageSize: 5 },
|
|
37
|
+
jpdcl_smart_notifications: {},
|
|
38
|
+
jpdcl_smart_nearby_offices: { latitude: 32.7266, longitude: 74.8570 },
|
|
39
|
+
jpdcl_smart_report: { report: "voltage", from: recent, to: today, end: 5 },
|
|
40
|
+
jpdcl_read: { endpoint: "main_complaint_types" },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const auditCommand = process.env.JPDCL_MCP_AUDIT_COMMAND || process.execPath;
|
|
44
|
+
const auditArgs = process.env.JPDCL_MCP_AUDIT_ARGS
|
|
45
|
+
? JSON.parse(process.env.JPDCL_MCP_AUDIT_ARGS)
|
|
46
|
+
: [new URL("../dist/mcp-stdio.js", import.meta.url).pathname];
|
|
47
|
+
|
|
48
|
+
const transport = new StdioClientTransport({
|
|
49
|
+
command: auditCommand,
|
|
50
|
+
args: auditArgs,
|
|
51
|
+
cwd: new URL("..", import.meta.url).pathname,
|
|
52
|
+
env: { ...process.env },
|
|
53
|
+
stderr: "pipe",
|
|
54
|
+
});
|
|
55
|
+
const client = new Client({ name: "jpdcl-mcp-audit", version: "1.0.0" });
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await client.connect(transport);
|
|
59
|
+
if (client.getServerVersion()?.name !== "JPDCL Smart Meter") {
|
|
60
|
+
throw new Error(`Unexpected MCP server name: ${client.getServerVersion()?.name ?? "missing"}`);
|
|
61
|
+
}
|
|
62
|
+
const listed = await client.listTools();
|
|
63
|
+
const names = listed.tools.map((tool) => tool.name);
|
|
64
|
+
const duplicates = names.filter((name, index) => names.indexOf(name) !== index);
|
|
65
|
+
const missingMetadata = listed.tools
|
|
66
|
+
.filter((tool) => !tool.title || !tool.description || !tool.annotations)
|
|
67
|
+
.map((tool) => tool.name);
|
|
68
|
+
const untested = names.filter((name) => name !== "jpdcl_auth_login" && !(name in cases) && name !== "jpdcl_mutate");
|
|
69
|
+
const unknownCases = Object.keys(cases).filter((name) => !names.includes(name));
|
|
70
|
+
if (duplicates.length || missingMetadata.length || untested.length || unknownCases.length) {
|
|
71
|
+
throw new Error(JSON.stringify({ duplicates, missingMetadata, untested, unknownCases }));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const authTool = listed.tools.find((tool) => tool.name === "jpdcl_auth_login");
|
|
75
|
+
const authRequired = authTool?.inputSchema?.required ?? [];
|
|
76
|
+
if (!authRequired.includes("loginId") || !authRequired.includes("password")) {
|
|
77
|
+
throw new Error("jpdcl_auth_login must require loginId and password");
|
|
78
|
+
}
|
|
79
|
+
let authenticationCheck = "schema-only (no configured credentials)";
|
|
80
|
+
const { resolveCredentials } = await import("../dist/credentials.js");
|
|
81
|
+
const credentials = await resolveCredentials();
|
|
82
|
+
if (credentials) {
|
|
83
|
+
const authentication = await client.callTool({
|
|
84
|
+
name: "jpdcl_auth_login",
|
|
85
|
+
arguments: { loginId: credentials.loginId, password: credentials.password, saveToEnv: false },
|
|
86
|
+
});
|
|
87
|
+
if (authentication.isError || !authentication.structuredContent?.authenticated) {
|
|
88
|
+
throw new Error("jpdcl_auth_login failed with the configured credentials");
|
|
89
|
+
}
|
|
90
|
+
authenticationCheck = "live login passed without rewriting credentials";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const results = [];
|
|
94
|
+
const upstreamUnavailable = [];
|
|
95
|
+
for (const [name, args] of Object.entries(cases)) {
|
|
96
|
+
const started = Date.now();
|
|
97
|
+
const response = await client.callTool({ name, arguments: args }, undefined, { timeout: 45_000 });
|
|
98
|
+
results.push({ name, ok: !response.isError, milliseconds: Date.now() - started });
|
|
99
|
+
if (response.isError) throw new Error(`${name} failed its live read-only audit`);
|
|
100
|
+
if (!response.structuredContent) throw new Error(`${name} returned no structuredContent`);
|
|
101
|
+
if (response.structuredContent?._meta?.available === false) upstreamUnavailable.push(name);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const mutationGuard = await client.callTool({
|
|
105
|
+
name: "jpdcl_mutate",
|
|
106
|
+
arguments: { endpoint: "main_update_contact", confirm: false },
|
|
107
|
+
});
|
|
108
|
+
if (!mutationGuard.isError || !mutationGuard.content?.some((block) => block.type === "text" && block.text.includes("confirm=true"))) {
|
|
109
|
+
throw new Error("jpdcl_mutate explicit-confirmation guard failed");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const derivedGuard = await client.callTool({
|
|
113
|
+
name: "jpdcl_read",
|
|
114
|
+
arguments: { endpoint: "smart_forecast_today" },
|
|
115
|
+
});
|
|
116
|
+
if (!derivedGuard.isError || !derivedGuard.content?.some((block) => block.type === "text" && block.text.includes("allowDerived=true"))) {
|
|
117
|
+
throw new Error("jpdcl_read derived-data guard failed");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const resources = await client.listResources();
|
|
121
|
+
for (const uri of ["jpdcl://catalog", "jpdcl://guide"]) {
|
|
122
|
+
if (!resources.resources.some((resource) => resource.uri === uri)) throw new Error(`Missing MCP resource ${uri}`);
|
|
123
|
+
const resource = await client.readResource({ uri });
|
|
124
|
+
if (!resource.contents.length) throw new Error(`Empty MCP resource ${uri}`);
|
|
125
|
+
}
|
|
126
|
+
if (!client.getInstructions()?.includes("jpdcl_snapshot")) throw new Error("MCP initialization instructions are missing");
|
|
127
|
+
|
|
128
|
+
process.stdout.write(`${JSON.stringify({
|
|
129
|
+
status: "ok",
|
|
130
|
+
toolCount: names.length,
|
|
131
|
+
liveReadToolsPassed: results.length,
|
|
132
|
+
authenticationCheck,
|
|
133
|
+
guardedToolsPassed: ["jpdcl_mutate confirmation", "jpdcl_read derived-data opt-in"],
|
|
134
|
+
resourcesPassed: ["jpdcl://catalog", "jpdcl://guide"],
|
|
135
|
+
upstreamUnavailable,
|
|
136
|
+
slowest: [...results].sort((a, b) => b.milliseconds - a.milliseconds).slice(0, 5),
|
|
137
|
+
}, null, 2)}\n`);
|
|
138
|
+
} finally {
|
|
139
|
+
await client.close();
|
|
140
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { listEndpoints } from "../src/catalog.ts";
|
|
2
|
+
|
|
3
|
+
const origin = process.env.JPDCL_SMART_ORIGIN ?? "https://cp.rdssjpdcl.com";
|
|
4
|
+
const htmlResponse = await fetch(`${origin}/`);
|
|
5
|
+
if (!htmlResponse.ok) throw new Error(`Smart portal returned HTTP ${htmlResponse.status}`);
|
|
6
|
+
const html = await htmlResponse.text();
|
|
7
|
+
const assetPath = html.match(/(?:src=["'])(\/assets\/index-[^"']+\.js)/)?.[1];
|
|
8
|
+
if (!assetPath) throw new Error("Unable to locate the current smart-portal application bundle");
|
|
9
|
+
|
|
10
|
+
const bundleResponse = await fetch(new URL(assetPath, origin));
|
|
11
|
+
if (!bundleResponse.ok) throw new Error(`Smart-portal bundle returned HTTP ${bundleResponse.status}`);
|
|
12
|
+
const bundle = await bundleResponse.text();
|
|
13
|
+
|
|
14
|
+
const routes = [
|
|
15
|
+
"/", "/login", "/home", "/usage", "/usage/reports", "/usage/reports/:reportId",
|
|
16
|
+
"/usage/calculator", "/pay", "/pay/recharge", "/pay/payment", "/pay/autopay",
|
|
17
|
+
"/pay/bill", "/pay/success", "/payment/return", "/support", "/accounts",
|
|
18
|
+
"/know-your-meter", "/energy-saving-tips", "/dashboard", "/prepaid/*", "/postpaid/*",
|
|
19
|
+
"/admin", "/admin/dashboard", "/admin/token", "/admin/login-counts", "/admin/feature",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const panels = [
|
|
23
|
+
"Meter Details", "Connection", "Locate Office", "My Connections", "Payment Methods",
|
|
24
|
+
"Notification Preferences", "My Alerts", "Frequently Asked Questions", "About Department",
|
|
25
|
+
"Terms and Conditions", "Privacy Policy",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const missingRoutes = routes.filter((route) => !bundle.includes(`path:\"${route}\"`));
|
|
29
|
+
const missingPanels = panels.filter((panel) => !bundle.includes(panel));
|
|
30
|
+
|
|
31
|
+
function staticFragments(path) {
|
|
32
|
+
return path
|
|
33
|
+
.split(/\{[^}]+\}/)
|
|
34
|
+
.flatMap((part) => part.split(/[?&=]/))
|
|
35
|
+
.map((part) => part.replace(/\/$/, ""))
|
|
36
|
+
.filter((part) => part.length >= 4 && part !== "true" && part !== "false");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fragmentExists(fragment) {
|
|
40
|
+
if (bundle.includes(fragment)) return true;
|
|
41
|
+
const pieces = fragment.split("/").filter((piece) => piece.length >= 4);
|
|
42
|
+
return pieces.length > 0 && pieces.every((piece) => bundle.includes(piece));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const smartEndpoints = listEndpoints("smart");
|
|
46
|
+
const missingEndpoints = smartEndpoints
|
|
47
|
+
.filter((endpoint) => !staticFragments(endpoint.path).every(fragmentExists))
|
|
48
|
+
.map((endpoint) => ({ name: endpoint.name, path: endpoint.path }));
|
|
49
|
+
|
|
50
|
+
const result = {
|
|
51
|
+
origin,
|
|
52
|
+
assetPath,
|
|
53
|
+
routesChecked: routes.length,
|
|
54
|
+
panelsChecked: panels.length,
|
|
55
|
+
smartEndpointsChecked: smartEndpoints.length,
|
|
56
|
+
missingRoutes,
|
|
57
|
+
missingPanels,
|
|
58
|
+
missingEndpoints,
|
|
59
|
+
passed: missingRoutes.length === 0 && missingPanels.length === 0 && missingEndpoints.length === 0,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
63
|
+
if (!result.passed) process.exitCode = 1;
|