tako-mcp 0.2.0 → 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/README.md +15 -40
- package/dist/index.js +171 -388
- package/package.json +2 -3
- package/dist/get-credentials.js +0 -89
package/README.md
CHANGED
|
@@ -1,48 +1,27 @@
|
|
|
1
1
|
# tako-mcp
|
|
2
2
|
|
|
3
|
-
[Octopus Energy
|
|
3
|
+
[Octopus Energy Japan](https://octopusenergy.co.jp/) GraphQL API の MCP サーバー。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
電力の使用量・コスト見積、契約情報、料金プランなどを MCP ツール経由で取得できます。Claude をはじめとする LLM でのエネルギー使用状況の分析に活用できます。
|
|
6
6
|
|
|
7
7
|
## 特徴
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
- 郵便番号からのグリッド供給ポイント検索
|
|
13
|
-
- MPAN/MPRN/シリアル番号の自動解決(LLM コンテキストには露出しない)
|
|
9
|
+
- 30分単位の電力消費量・コスト見積の取得
|
|
10
|
+
- アカウント・契約情報の取得(プロダクト・基本料金含む)
|
|
11
|
+
- 郵便番号からのエリア情報検索
|
|
14
12
|
- プライバシー重視: 住所等の個人情報はツールレスポンスから除外
|
|
15
13
|
|
|
16
14
|
## 必要条件
|
|
17
15
|
|
|
18
16
|
- Node.js 22+
|
|
19
|
-
- Octopus Energy
|
|
20
|
-
|
|
21
|
-
## セットアップ
|
|
22
|
-
|
|
23
|
-
### API キーとアカウント番号の取得
|
|
24
|
-
|
|
25
|
-
メールアドレスとパスワードから、Kraken GraphQL API 経由で取得できます。
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
npx tako-mcp-get-credentials
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
対話的に Email / Password を入力すると、`OCTOPUS_API_KEY` と `OCTOPUS_ACCOUNT_NUMBER` が標準出力に出力されます。
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
# .env ファイルに直接書き出す場合
|
|
35
|
-
npx tako-mcp-get-credentials > .env
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
デフォルトでは Octopus Energy Japan の API (`https://api.oejp-kraken.energy/v1/graphql/`) を使用します。他リージョンの場合は `KRAKEN_GRAPHQL_URL` 環境変数で上書きしてください。
|
|
17
|
+
- Octopus Energy Japan のアカウント(メールアドレス・パスワード)
|
|
39
18
|
|
|
40
19
|
## 使い方
|
|
41
20
|
|
|
42
21
|
デフォルトは stdio トランスポートで動作します。
|
|
43
22
|
|
|
44
23
|
```bash
|
|
45
|
-
|
|
24
|
+
OCTOPUS_EMAIL=you@example.com OCTOPUS_PASSWORD=your-password OCTOPUS_ACCOUNT_NUMBER=A-AAAA1111 npx tako-mcp
|
|
46
25
|
```
|
|
47
26
|
|
|
48
27
|
### HTTP モード
|
|
@@ -50,7 +29,7 @@ OCTOPUS_API_KEY=<token> OCTOPUS_ACCOUNT_NUMBER=A-AAAA1111 npx tako-mcp
|
|
|
50
29
|
`--http` フラグで Streamable HTTP トランスポートに切り替えられます。
|
|
51
30
|
|
|
52
31
|
```bash
|
|
53
|
-
|
|
32
|
+
OCTOPUS_EMAIL=you@example.com OCTOPUS_PASSWORD=your-password OCTOPUS_ACCOUNT_NUMBER=A-AAAA1111 npx tako-mcp --http
|
|
54
33
|
```
|
|
55
34
|
|
|
56
35
|
`PORT` 未指定時は空きポートが自動選択されます。
|
|
@@ -59,7 +38,8 @@ OCTOPUS_API_KEY=<token> OCTOPUS_ACCOUNT_NUMBER=A-AAAA1111 npx tako-mcp --http
|
|
|
59
38
|
|
|
60
39
|
| 変数名 | 必須 | 説明 |
|
|
61
40
|
|---|---|---|
|
|
62
|
-
| `
|
|
41
|
+
| `OCTOPUS_EMAIL` | Yes | Octopus Energy Japan のログインメールアドレス |
|
|
42
|
+
| `OCTOPUS_PASSWORD` | Yes | Octopus Energy Japan のログインパスワード |
|
|
63
43
|
| `OCTOPUS_ACCOUNT_NUMBER` | Yes | アカウント番号(例: `A-AAAA1111`) |
|
|
64
44
|
| `PORT` | No | HTTP モード時のポート番号(デフォルト: 自動選択) |
|
|
65
45
|
|
|
@@ -67,15 +47,9 @@ OCTOPUS_API_KEY=<token> OCTOPUS_ACCOUNT_NUMBER=A-AAAA1111 npx tako-mcp --http
|
|
|
67
47
|
|
|
68
48
|
| ツール | 説明 | 認証 |
|
|
69
49
|
|---|---|---|
|
|
70
|
-
| `get_account_info` |
|
|
71
|
-
| `get_electricity_consumption` |
|
|
72
|
-
| `
|
|
73
|
-
| `list_products` | 利用可能な料金プロダクト一覧 | 不要 |
|
|
74
|
-
| `get_product` | プロダクト詳細(地域別料金含む) | 不要 |
|
|
75
|
-
| `get_electricity_rates` | 電気料金の単位料金(standard/day/night) | 不要 |
|
|
76
|
-
| `get_gas_rates` | ガス料金の単位料金 | 不要 |
|
|
77
|
-
| `get_standing_charges` | 電気・ガスの基本料金 | 不要 |
|
|
78
|
-
| `get_grid_supply_point` | 郵便番号からグリッド供給ポイントを検索 | 不要 |
|
|
50
|
+
| `get_account_info` | アカウント・契約情報(プロダクト・基本料金含む、住所は除外) | 必要 |
|
|
51
|
+
| `get_electricity_consumption` | 30分単位の電力消費量・コスト見積 | 必要 |
|
|
52
|
+
| `get_postal_areas` | 郵便番号からエリア情報を検索 | 不要 |
|
|
79
53
|
|
|
80
54
|
## クライアント設定
|
|
81
55
|
|
|
@@ -88,7 +62,8 @@ OCTOPUS_API_KEY=<token> OCTOPUS_ACCOUNT_NUMBER=A-AAAA1111 npx tako-mcp --http
|
|
|
88
62
|
"command": "npx",
|
|
89
63
|
"args": ["-y", "tako-mcp"],
|
|
90
64
|
"env": {
|
|
91
|
-
"
|
|
65
|
+
"OCTOPUS_EMAIL": "you@example.com",
|
|
66
|
+
"OCTOPUS_PASSWORD": "your-password",
|
|
92
67
|
"OCTOPUS_ACCOUNT_NUMBER": "A-AAAA1111"
|
|
93
68
|
}
|
|
94
69
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,234 +4,162 @@
|
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
|
|
6
6
|
// src/client.ts
|
|
7
|
-
var BASE_URL = "https://api.
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
meterInfoCache = null;
|
|
13
|
-
constructor(apiKey2, accountNumber2) {
|
|
14
|
-
this.apiKey = apiKey2;
|
|
15
|
-
this.accountNumber = accountNumber2;
|
|
16
|
-
this.authHeader = `Basic ${btoa(`${this.apiKey}:`)}`;
|
|
7
|
+
var BASE_URL = "https://api.oejp-kraken.energy/v1/graphql/";
|
|
8
|
+
var OBTAIN_TOKEN_MUTATION = `
|
|
9
|
+
mutation obtainKrakenToken($input: ObtainJSONWebTokenInput!) {
|
|
10
|
+
obtainKrakenToken(input: $input) {
|
|
11
|
+
token
|
|
17
12
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
var ACCOUNT_QUERY = `
|
|
16
|
+
query account($accountNumber: String!) {
|
|
17
|
+
account(accountNumber: $accountNumber) {
|
|
18
|
+
number
|
|
19
|
+
status
|
|
20
|
+
balance
|
|
21
|
+
properties {
|
|
22
|
+
id
|
|
23
|
+
address
|
|
24
|
+
electricitySupplyPoints {
|
|
25
|
+
spin
|
|
26
|
+
status
|
|
27
|
+
meters {
|
|
28
|
+
serialNumber
|
|
29
|
+
}
|
|
30
|
+
agreements {
|
|
31
|
+
id
|
|
32
|
+
validFrom
|
|
33
|
+
validTo
|
|
34
|
+
product {
|
|
35
|
+
... on ProductInterface {
|
|
36
|
+
code
|
|
37
|
+
displayName
|
|
38
|
+
standingChargePricePerDay
|
|
39
|
+
}
|
|
40
|
+
}
|
|
24
41
|
}
|
|
25
42
|
}
|
|
26
43
|
}
|
|
27
|
-
const headers = {};
|
|
28
|
-
if (options?.auth !== false) {
|
|
29
|
-
headers.Authorization = this.authHeader;
|
|
30
|
-
}
|
|
31
|
-
const response = await fetch(url.toString(), { headers });
|
|
32
|
-
if (!response.ok) {
|
|
33
|
-
throw new Error(
|
|
34
|
-
`Octopus Energy API error: ${response.status} ${response.statusText}`
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
return response.json();
|
|
38
44
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
var HALF_HOURLY_READINGS_QUERY = `
|
|
48
|
+
query halfHourlyReadings($accountNumber: String!, $fromDatetime: DateTime, $toDatetime: DateTime) {
|
|
49
|
+
account(accountNumber: $accountNumber) {
|
|
50
|
+
properties {
|
|
51
|
+
electricitySupplyPoints {
|
|
52
|
+
halfHourlyReadings(fromDatetime: $fromDatetime, toDatetime: $toDatetime) {
|
|
53
|
+
startAt
|
|
54
|
+
endAt
|
|
55
|
+
value
|
|
56
|
+
costEstimate
|
|
57
|
+
consumptionStep
|
|
58
|
+
consumptionRateBand
|
|
48
59
|
}
|
|
49
60
|
}
|
|
50
|
-
url = u.toString();
|
|
51
|
-
}
|
|
52
|
-
while (url) {
|
|
53
|
-
const headers = {};
|
|
54
|
-
if (options?.auth !== false) {
|
|
55
|
-
headers.Authorization = this.authHeader;
|
|
56
|
-
}
|
|
57
|
-
const response = await fetch(url, { headers });
|
|
58
|
-
if (!response.ok) {
|
|
59
|
-
throw new Error(
|
|
60
|
-
`Octopus Energy API error: ${response.status} ${response.statusText}`
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
const data = await response.json();
|
|
64
|
-
results.push(...data.results);
|
|
65
|
-
url = data.next;
|
|
66
61
|
}
|
|
67
|
-
return results;
|
|
68
62
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
const elecPoint = activeProperty.electricity_meter_points[0];
|
|
80
|
-
const gasPoint = activeProperty.gas_meter_points[0];
|
|
81
|
-
this.meterInfoCache = {
|
|
82
|
-
electricity: elecPoint ? {
|
|
83
|
-
mpan: elecPoint.mpan,
|
|
84
|
-
serialNumber: elecPoint.meters[0]?.serial_number ?? ""
|
|
85
|
-
} : null,
|
|
86
|
-
gas: gasPoint ? {
|
|
87
|
-
mprn: gasPoint.mprn,
|
|
88
|
-
serialNumber: gasPoint.meters[0]?.serial_number ?? ""
|
|
89
|
-
} : null
|
|
90
|
-
};
|
|
91
|
-
return this.meterInfoCache;
|
|
92
|
-
}
|
|
93
|
-
async getAccount() {
|
|
94
|
-
return this.fetch(`/accounts/${this.accountNumber}/`);
|
|
95
|
-
}
|
|
96
|
-
async getElectricityConsumption(params) {
|
|
97
|
-
const meterInfo = await this.resolveMeterInfo();
|
|
98
|
-
if (!meterInfo.electricity) {
|
|
99
|
-
throw new Error("No electricity meter found on account");
|
|
100
|
-
}
|
|
101
|
-
const { mpan, serialNumber } = meterInfo.electricity;
|
|
102
|
-
const queryParams = {
|
|
103
|
-
period_from: params.period_from,
|
|
104
|
-
period_to: params.period_to
|
|
105
|
-
};
|
|
106
|
-
if (params.group_by) {
|
|
107
|
-
queryParams.group_by = params.group_by;
|
|
108
|
-
}
|
|
109
|
-
if (params.order_by) {
|
|
110
|
-
queryParams.order_by = params.order_by;
|
|
111
|
-
}
|
|
112
|
-
if (params.page_size) {
|
|
113
|
-
queryParams.page_size = String(params.page_size);
|
|
114
|
-
}
|
|
115
|
-
return this.fetchAllPages(
|
|
116
|
-
`/electricity-meter-points/${mpan}/meters/${serialNumber}/consumption/`,
|
|
117
|
-
{ params: queryParams }
|
|
118
|
-
);
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
var POSTAL_AREAS_QUERY = `
|
|
66
|
+
query postalAreas($postcode: String!) {
|
|
67
|
+
postalAreas(postcode: $postcode) {
|
|
68
|
+
postcode
|
|
69
|
+
prefecture
|
|
70
|
+
city
|
|
71
|
+
area
|
|
119
72
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (params.order_by) {
|
|
134
|
-
queryParams.order_by = params.order_by;
|
|
135
|
-
}
|
|
136
|
-
if (params.page_size) {
|
|
137
|
-
queryParams.page_size = String(params.page_size);
|
|
138
|
-
}
|
|
139
|
-
return this.fetchAllPages(
|
|
140
|
-
`/gas-meter-points/${mprn}/meters/${serialNumber}/consumption/`,
|
|
141
|
-
{ params: queryParams }
|
|
142
|
-
);
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
var TOKEN_TTL_MS = 55 * 60 * 1e3;
|
|
76
|
+
var OctopusClient = class {
|
|
77
|
+
email;
|
|
78
|
+
password;
|
|
79
|
+
accountNumber;
|
|
80
|
+
cachedToken = null;
|
|
81
|
+
tokenExpiresAt = 0;
|
|
82
|
+
constructor(email2, password2, accountNumber2) {
|
|
83
|
+
this.email = email2;
|
|
84
|
+
this.password = password2;
|
|
85
|
+
this.accountNumber = accountNumber2;
|
|
143
86
|
}
|
|
144
|
-
async
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
params.is_variable = String(filter.is_variable);
|
|
148
|
-
}
|
|
149
|
-
if (filter?.is_green !== void 0) {
|
|
150
|
-
params.is_green = String(filter.is_green);
|
|
87
|
+
async obtainToken() {
|
|
88
|
+
if (this.cachedToken && Date.now() < this.tokenExpiresAt) {
|
|
89
|
+
return this.cachedToken;
|
|
151
90
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
if (filter?.available_at) {
|
|
156
|
-
params.available_at = filter.available_at;
|
|
157
|
-
}
|
|
158
|
-
if (filter?.is_prepay !== void 0) {
|
|
159
|
-
params.is_prepay = String(filter.is_prepay);
|
|
160
|
-
}
|
|
161
|
-
return this.fetchAllPages("/products/", {
|
|
162
|
-
auth: false,
|
|
163
|
-
params
|
|
91
|
+
const data = await this.graphqlPublic(OBTAIN_TOKEN_MUTATION, {
|
|
92
|
+
input: { email: this.email, password: this.password }
|
|
164
93
|
});
|
|
94
|
+
this.cachedToken = data.obtainKrakenToken.token;
|
|
95
|
+
this.tokenExpiresAt = Date.now() + TOKEN_TTL_MS;
|
|
96
|
+
return this.cachedToken;
|
|
165
97
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
auth: false,
|
|
173
|
-
params
|
|
98
|
+
/** 認証なしの GraphQL リクエスト */
|
|
99
|
+
async graphqlPublic(query, variables) {
|
|
100
|
+
const response = await fetch(BASE_URL, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/json" },
|
|
103
|
+
body: JSON.stringify({ query, variables })
|
|
174
104
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (params?.period_from) {
|
|
180
|
-
queryParams.period_from = params.period_from;
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`GraphQL HTTP error: ${response.status} ${response.statusText}`
|
|
108
|
+
);
|
|
181
109
|
}
|
|
182
|
-
|
|
183
|
-
|
|
110
|
+
const json = await response.json();
|
|
111
|
+
if (json.errors?.length) {
|
|
112
|
+
throw new Error(json.errors.map((e) => e.message).join(", "));
|
|
184
113
|
}
|
|
185
|
-
if (
|
|
186
|
-
|
|
114
|
+
if (!json.data) {
|
|
115
|
+
throw new Error("No data in GraphQL response");
|
|
187
116
|
}
|
|
188
|
-
return
|
|
189
|
-
`/products/${productCode}/electricity-tariffs/${tariffCode}/${rateSegment}/`,
|
|
190
|
-
{ auth: false, params: queryParams }
|
|
191
|
-
);
|
|
117
|
+
return json.data;
|
|
192
118
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
119
|
+
/** 認証付き GraphQL リクエスト */
|
|
120
|
+
async graphql(query, variables) {
|
|
121
|
+
const token = await this.obtainToken();
|
|
122
|
+
const response = await fetch(BASE_URL, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: {
|
|
125
|
+
"Content-Type": "application/json",
|
|
126
|
+
Authorization: token
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify({ query, variables })
|
|
129
|
+
});
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`GraphQL HTTP error: ${response.status} ${response.statusText}`
|
|
133
|
+
);
|
|
197
134
|
}
|
|
198
|
-
|
|
199
|
-
|
|
135
|
+
const json = await response.json();
|
|
136
|
+
if (json.errors?.length) {
|
|
137
|
+
throw new Error(json.errors.map((e) => e.message).join(", "));
|
|
200
138
|
}
|
|
201
|
-
if (
|
|
202
|
-
|
|
139
|
+
if (!json.data) {
|
|
140
|
+
throw new Error("No data in GraphQL response");
|
|
203
141
|
}
|
|
204
|
-
return
|
|
205
|
-
`/products/${productCode}/gas-tariffs/${tariffCode}/standard-unit-rates/`,
|
|
206
|
-
{ auth: false, params: queryParams }
|
|
207
|
-
);
|
|
142
|
+
return json.data;
|
|
208
143
|
}
|
|
209
|
-
async
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}
|
|
215
|
-
if (params?.period_to) {
|
|
216
|
-
queryParams.period_to = params.period_to;
|
|
217
|
-
}
|
|
218
|
-
if (params?.page_size) {
|
|
219
|
-
queryParams.page_size = String(params.page_size);
|
|
220
|
-
}
|
|
221
|
-
return this.fetchAllPages(
|
|
222
|
-
`/products/${productCode}/${fuelSegment}/${tariffCode}/standing-charges/`,
|
|
223
|
-
{ auth: false, params: queryParams }
|
|
224
|
-
);
|
|
144
|
+
async getAccount() {
|
|
145
|
+
const data = await this.graphql(ACCOUNT_QUERY, {
|
|
146
|
+
accountNumber: this.accountNumber
|
|
147
|
+
});
|
|
148
|
+
return data.account;
|
|
225
149
|
}
|
|
226
|
-
async
|
|
227
|
-
const data = await this.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
150
|
+
async getElectricityConsumption(from, to) {
|
|
151
|
+
const data = await this.graphql(HALF_HOURLY_READINGS_QUERY, {
|
|
152
|
+
accountNumber: this.accountNumber,
|
|
153
|
+
fromDatetime: from,
|
|
154
|
+
toDatetime: to
|
|
155
|
+
});
|
|
156
|
+
return data.account.properties.flatMap(
|
|
157
|
+
(p) => p.electricitySupplyPoints.flatMap((sp) => sp.halfHourlyReadings)
|
|
233
158
|
);
|
|
234
|
-
|
|
159
|
+
}
|
|
160
|
+
async getPostalAreas(postcode) {
|
|
161
|
+
const data = await this.graphqlPublic(POSTAL_AREAS_QUERY, { postcode });
|
|
162
|
+
return data.postalAreas;
|
|
235
163
|
}
|
|
236
164
|
};
|
|
237
165
|
|
|
@@ -240,19 +168,20 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
240
168
|
|
|
241
169
|
// src/tools/account.ts
|
|
242
170
|
function registerAccountTools(server, client2) {
|
|
243
|
-
server.
|
|
171
|
+
server.registerTool(
|
|
244
172
|
"get_account_info",
|
|
245
|
-
|
|
173
|
+
{
|
|
174
|
+
description: "Get account information including contract status, electricity supply points, active agreements, product details and standing charges. Address is excluded for privacy."
|
|
175
|
+
},
|
|
246
176
|
async () => {
|
|
247
177
|
const account = await client2.getAccount();
|
|
248
178
|
const sanitized = {
|
|
249
179
|
number: account.number,
|
|
180
|
+
status: account.status,
|
|
181
|
+
balance: account.balance,
|
|
250
182
|
properties: account.properties.map((p) => ({
|
|
251
183
|
id: p.id,
|
|
252
|
-
|
|
253
|
-
moved_out_at: p.moved_out_at,
|
|
254
|
-
electricity_meter_points: p.electricity_meter_points,
|
|
255
|
-
gas_meter_points: p.gas_meter_points
|
|
184
|
+
electricitySupplyPoints: p.electricitySupplyPoints
|
|
256
185
|
}))
|
|
257
186
|
};
|
|
258
187
|
return {
|
|
@@ -264,110 +193,19 @@ function registerAccountTools(server, client2) {
|
|
|
264
193
|
);
|
|
265
194
|
}
|
|
266
195
|
|
|
267
|
-
// src/tools/
|
|
196
|
+
// src/tools/area.ts
|
|
268
197
|
import { z } from "zod";
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
"
|
|
272
|
-
),
|
|
273
|
-
period_to: z.string().describe("End datetime in ISO 8601 format (UTC), e.g. 2024-02-01T00:00Z"),
|
|
274
|
-
group_by: z.enum(["day", "week", "month", "quarter"]).optional().describe("Group consumption data by time period")
|
|
275
|
-
};
|
|
276
|
-
function registerConsumptionTools(server, client2) {
|
|
277
|
-
server.tool(
|
|
278
|
-
"get_electricity_consumption",
|
|
279
|
-
"Get electricity consumption data for the account. MPAN and serial number are resolved automatically.",
|
|
280
|
-
consumptionParams,
|
|
281
|
-
async (args) => {
|
|
282
|
-
const data = await client2.getElectricityConsumption({
|
|
283
|
-
period_from: args.period_from,
|
|
284
|
-
period_to: args.period_to,
|
|
285
|
-
group_by: args.group_by
|
|
286
|
-
});
|
|
287
|
-
return {
|
|
288
|
-
content: [
|
|
289
|
-
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
290
|
-
]
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
);
|
|
294
|
-
server.tool(
|
|
295
|
-
"get_gas_consumption",
|
|
296
|
-
"Get gas consumption data for the account. MPRN and serial number are resolved automatically.",
|
|
297
|
-
consumptionParams,
|
|
298
|
-
async (args) => {
|
|
299
|
-
const data = await client2.getGasConsumption({
|
|
300
|
-
period_from: args.period_from,
|
|
301
|
-
period_to: args.period_to,
|
|
302
|
-
group_by: args.group_by
|
|
303
|
-
});
|
|
304
|
-
return {
|
|
305
|
-
content: [
|
|
306
|
-
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
307
|
-
]
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// src/tools/grid.ts
|
|
314
|
-
import { z as z2 } from "zod";
|
|
315
|
-
function registerGridTools(server, client2) {
|
|
316
|
-
server.tool(
|
|
317
|
-
"get_grid_supply_point",
|
|
318
|
-
"Look up Grid Supply Point (GSP) information by postcode. Useful for determining the regional electricity distribution zone. No authentication required.",
|
|
319
|
-
{
|
|
320
|
-
postcode: z2.string().describe("UK postcode, e.g. SW1A 1AA")
|
|
321
|
-
},
|
|
322
|
-
async (args) => {
|
|
323
|
-
const data = await client2.getGridSupplyPoint(args.postcode);
|
|
324
|
-
return {
|
|
325
|
-
content: [
|
|
326
|
-
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
327
|
-
]
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// src/tools/products.ts
|
|
334
|
-
import { z as z3 } from "zod";
|
|
335
|
-
function registerProductTools(server, client2) {
|
|
336
|
-
server.tool(
|
|
337
|
-
"list_products",
|
|
338
|
-
"List available energy products/tariffs. No authentication required.",
|
|
339
|
-
{
|
|
340
|
-
is_variable: z3.boolean().optional().describe("Filter by variable tariffs"),
|
|
341
|
-
is_green: z3.boolean().optional().describe("Filter by green tariffs"),
|
|
342
|
-
is_business: z3.boolean().optional().describe("Filter by business tariffs"),
|
|
343
|
-
available_at: z3.string().optional().describe("Filter products available at this datetime (ISO 8601)")
|
|
344
|
-
},
|
|
345
|
-
async (args) => {
|
|
346
|
-
const data = await client2.listProducts({
|
|
347
|
-
is_variable: args.is_variable,
|
|
348
|
-
is_green: args.is_green,
|
|
349
|
-
is_business: args.is_business,
|
|
350
|
-
available_at: args.available_at
|
|
351
|
-
});
|
|
352
|
-
return {
|
|
353
|
-
content: [
|
|
354
|
-
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
355
|
-
]
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
);
|
|
359
|
-
server.tool(
|
|
360
|
-
"get_product",
|
|
361
|
-
"Get detailed information about a specific product, including regional tariff details. No authentication required.",
|
|
198
|
+
function registerAreaTools(server, client2) {
|
|
199
|
+
server.registerTool(
|
|
200
|
+
"get_postal_areas",
|
|
362
201
|
{
|
|
363
|
-
|
|
364
|
-
|
|
202
|
+
description: "Look up area information by Japanese postcode. Returns prefecture, city, and area. No authentication required.",
|
|
203
|
+
inputSchema: {
|
|
204
|
+
postcode: z.string().describe("Japanese postcode, e.g. 916-0045")
|
|
205
|
+
}
|
|
365
206
|
},
|
|
366
207
|
async (args) => {
|
|
367
|
-
const data = await client2.
|
|
368
|
-
args.product_code,
|
|
369
|
-
args.tariffs_active_at
|
|
370
|
-
);
|
|
208
|
+
const data = await client2.getPostalAreas(args.postcode);
|
|
371
209
|
return {
|
|
372
210
|
content: [
|
|
373
211
|
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
@@ -377,80 +215,26 @@ function registerProductTools(server, client2) {
|
|
|
377
215
|
);
|
|
378
216
|
}
|
|
379
217
|
|
|
380
|
-
// src/tools/
|
|
381
|
-
import { z as
|
|
382
|
-
function
|
|
383
|
-
server.
|
|
384
|
-
"
|
|
385
|
-
"Get electricity unit rates for a product/tariff. Supports Agile's half-hourly pricing. No authentication required.",
|
|
386
|
-
{
|
|
387
|
-
product_code: z4.string().describe("The product code"),
|
|
388
|
-
tariff_code: z4.string().describe("The tariff code, e.g. E-1R-AGILE-FLEX-22-11-25-C"),
|
|
389
|
-
rate_type: z4.enum(["standard", "day", "night"]).optional().describe("Rate type (default: standard)"),
|
|
390
|
-
period_from: z4.string().optional().describe("Start datetime (ISO 8601)"),
|
|
391
|
-
period_to: z4.string().optional().describe("End datetime (ISO 8601)")
|
|
392
|
-
},
|
|
393
|
-
async (args) => {
|
|
394
|
-
const data = await client2.getElectricityRates(
|
|
395
|
-
args.product_code,
|
|
396
|
-
args.tariff_code,
|
|
397
|
-
args.rate_type ?? "standard",
|
|
398
|
-
{
|
|
399
|
-
period_from: args.period_from,
|
|
400
|
-
period_to: args.period_to
|
|
401
|
-
}
|
|
402
|
-
);
|
|
403
|
-
return {
|
|
404
|
-
content: [
|
|
405
|
-
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
406
|
-
]
|
|
407
|
-
};
|
|
408
|
-
}
|
|
409
|
-
);
|
|
410
|
-
server.tool(
|
|
411
|
-
"get_gas_rates",
|
|
412
|
-
"Get gas unit rates for a product/tariff. No authentication required.",
|
|
413
|
-
{
|
|
414
|
-
product_code: z4.string().describe("The product code"),
|
|
415
|
-
tariff_code: z4.string().describe("The tariff code, e.g. G-1R-VAR-17-01-11-D"),
|
|
416
|
-
period_from: z4.string().optional().describe("Start datetime (ISO 8601)"),
|
|
417
|
-
period_to: z4.string().optional().describe("End datetime (ISO 8601)")
|
|
418
|
-
},
|
|
419
|
-
async (args) => {
|
|
420
|
-
const data = await client2.getGasRates(
|
|
421
|
-
args.product_code,
|
|
422
|
-
args.tariff_code,
|
|
423
|
-
{
|
|
424
|
-
period_from: args.period_from,
|
|
425
|
-
period_to: args.period_to
|
|
426
|
-
}
|
|
427
|
-
);
|
|
428
|
-
return {
|
|
429
|
-
content: [
|
|
430
|
-
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
431
|
-
]
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
);
|
|
435
|
-
server.tool(
|
|
436
|
-
"get_standing_charges",
|
|
437
|
-
"Get standing charges (daily fixed charges) for a product/tariff. No authentication required.",
|
|
218
|
+
// src/tools/consumption.ts
|
|
219
|
+
import { z as z2 } from "zod";
|
|
220
|
+
function registerConsumptionTools(server, client2) {
|
|
221
|
+
server.registerTool(
|
|
222
|
+
"get_electricity_consumption",
|
|
438
223
|
{
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
224
|
+
description: "Get half-hourly electricity consumption data with cost estimates. Returns 30-minute interval readings for the specified period.",
|
|
225
|
+
inputSchema: {
|
|
226
|
+
period_from: z2.string().describe(
|
|
227
|
+
"Start datetime in ISO 8601 format, e.g. 2024-01-01T00:00:00+09:00"
|
|
228
|
+
),
|
|
229
|
+
period_to: z2.string().describe(
|
|
230
|
+
"End datetime in ISO 8601 format, e.g. 2024-02-01T00:00:00+09:00"
|
|
231
|
+
)
|
|
232
|
+
}
|
|
444
233
|
},
|
|
445
234
|
async (args) => {
|
|
446
|
-
const data = await client2.
|
|
447
|
-
args.
|
|
448
|
-
args.
|
|
449
|
-
args.fuel,
|
|
450
|
-
{
|
|
451
|
-
period_from: args.period_from,
|
|
452
|
-
period_to: args.period_to
|
|
453
|
-
}
|
|
235
|
+
const data = await client2.getElectricityConsumption(
|
|
236
|
+
args.period_from,
|
|
237
|
+
args.period_to
|
|
454
238
|
);
|
|
455
239
|
return {
|
|
456
240
|
content: [
|
|
@@ -465,26 +249,25 @@ function registerRateTools(server, client2) {
|
|
|
465
249
|
function createMcpServer(client2) {
|
|
466
250
|
const server = new McpServer({
|
|
467
251
|
name: "tako-mcp",
|
|
468
|
-
version: "
|
|
252
|
+
version: "1.0.0"
|
|
469
253
|
});
|
|
470
254
|
registerAccountTools(server, client2);
|
|
471
255
|
registerConsumptionTools(server, client2);
|
|
472
|
-
|
|
473
|
-
registerRateTools(server, client2);
|
|
474
|
-
registerGridTools(server, client2);
|
|
256
|
+
registerAreaTools(server, client2);
|
|
475
257
|
return server;
|
|
476
258
|
}
|
|
477
259
|
|
|
478
260
|
// src/index.ts
|
|
479
|
-
var
|
|
261
|
+
var email = process.env.OCTOPUS_EMAIL;
|
|
262
|
+
var password = process.env.OCTOPUS_PASSWORD;
|
|
480
263
|
var accountNumber = process.env.OCTOPUS_ACCOUNT_NUMBER;
|
|
481
|
-
if (!
|
|
264
|
+
if (!email || !password || !accountNumber) {
|
|
482
265
|
console.error(
|
|
483
|
-
"Missing required environment variables:
|
|
266
|
+
"Missing required environment variables: OCTOPUS_EMAIL, OCTOPUS_PASSWORD, OCTOPUS_ACCOUNT_NUMBER"
|
|
484
267
|
);
|
|
485
268
|
process.exit(1);
|
|
486
269
|
}
|
|
487
|
-
var client = new OctopusClient(
|
|
270
|
+
var client = new OctopusClient(email, password, accountNumber);
|
|
488
271
|
var mcpServer = createMcpServer(client);
|
|
489
272
|
if (process.argv.includes("--http")) {
|
|
490
273
|
const { StreamableHTTPTransport } = await import("@hono/mcp");
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tako-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "MCP server for Octopus Energy API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"tako-mcp": "dist/index.js"
|
|
8
|
-
"tako-mcp-get-credentials": "dist/get-credentials.js"
|
|
7
|
+
"tako-mcp": "dist/index.js"
|
|
9
8
|
},
|
|
10
9
|
"main": "./dist/index.js",
|
|
11
10
|
"files": [
|
package/dist/get-credentials.js
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/get-credentials.ts
|
|
4
|
-
import * as readline from "readline/promises";
|
|
5
|
-
var DEFAULT_GRAPHQL_URL = "https://api.oejp-kraken.energy/v1/graphql/";
|
|
6
|
-
var OBTAIN_TOKEN_MUTATION = `
|
|
7
|
-
mutation obtainKrakenToken($input: ObtainJSONWebTokenInput!) {
|
|
8
|
-
obtainKrakenToken(input: $input) {
|
|
9
|
-
token
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
`;
|
|
13
|
-
var GET_ACCOUNT_QUERY = `
|
|
14
|
-
query accountViewer {
|
|
15
|
-
viewer {
|
|
16
|
-
accounts {
|
|
17
|
-
number
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
`;
|
|
22
|
-
async function graphqlRequest(url, query, variables, token) {
|
|
23
|
-
const headers = {
|
|
24
|
-
"Content-Type": "application/json"
|
|
25
|
-
};
|
|
26
|
-
if (token) {
|
|
27
|
-
headers.authorization = `JWT ${token}`;
|
|
28
|
-
}
|
|
29
|
-
const response = await fetch(url, {
|
|
30
|
-
method: "POST",
|
|
31
|
-
headers,
|
|
32
|
-
body: JSON.stringify({ query, variables })
|
|
33
|
-
});
|
|
34
|
-
const data = await response.json();
|
|
35
|
-
if (data.errors?.length) {
|
|
36
|
-
throw new Error(data.errors.map((e) => e.message).join(", "));
|
|
37
|
-
}
|
|
38
|
-
if (!data.data) {
|
|
39
|
-
throw new Error("No data in GraphQL response");
|
|
40
|
-
}
|
|
41
|
-
return data.data;
|
|
42
|
-
}
|
|
43
|
-
async function getToken(url, email, password) {
|
|
44
|
-
const data = await graphqlRequest(url, OBTAIN_TOKEN_MUTATION, {
|
|
45
|
-
input: { email, password }
|
|
46
|
-
});
|
|
47
|
-
return data.obtainKrakenToken.token;
|
|
48
|
-
}
|
|
49
|
-
async function getAccountNumber(url, token) {
|
|
50
|
-
const data = await graphqlRequest(url, GET_ACCOUNT_QUERY, void 0, token);
|
|
51
|
-
const accounts = data.viewer.accounts;
|
|
52
|
-
if (accounts.length === 0) {
|
|
53
|
-
throw new Error("No accounts found");
|
|
54
|
-
}
|
|
55
|
-
return accounts[0].number;
|
|
56
|
-
}
|
|
57
|
-
async function prompt(rl, message) {
|
|
58
|
-
const answer = await rl.question(message);
|
|
59
|
-
return answer.trim();
|
|
60
|
-
}
|
|
61
|
-
async function main() {
|
|
62
|
-
const rl = readline.createInterface({
|
|
63
|
-
input: process.stdin,
|
|
64
|
-
output: process.stderr
|
|
65
|
-
});
|
|
66
|
-
try {
|
|
67
|
-
const graphqlUrl = process.env.KRAKEN_GRAPHQL_URL ?? DEFAULT_GRAPHQL_URL;
|
|
68
|
-
console.error(`Kraken GraphQL API: ${graphqlUrl}`);
|
|
69
|
-
console.error("");
|
|
70
|
-
const email = await prompt(rl, "Email: ");
|
|
71
|
-
const password = await prompt(rl, "Password: ");
|
|
72
|
-
console.error("");
|
|
73
|
-
console.error("Authenticating...");
|
|
74
|
-
const token = await getToken(graphqlUrl, email, password);
|
|
75
|
-
console.error("Fetching account number...");
|
|
76
|
-
const accountNumber = await getAccountNumber(graphqlUrl, token);
|
|
77
|
-
console.error("");
|
|
78
|
-
console.error("Done! Set the following environment variables:");
|
|
79
|
-
console.error("");
|
|
80
|
-
console.log(`OCTOPUS_API_KEY=${token}`);
|
|
81
|
-
console.log(`OCTOPUS_ACCOUNT_NUMBER=${accountNumber}`);
|
|
82
|
-
} finally {
|
|
83
|
-
rl.close();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
main().catch((err) => {
|
|
87
|
-
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
|
88
|
-
process.exit(1);
|
|
89
|
-
});
|