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 CHANGED
@@ -1,48 +1,27 @@
1
1
  # tako-mcp
2
2
 
3
- [Octopus Energy API](https://developer.octopus.energy/docs/api/) の MCP サーバー。
3
+ [Octopus Energy Japan](https://octopusenergy.co.jp/) GraphQL API の MCP サーバー。
4
4
 
5
- 電気・ガスの使用量、料金プラン、単価などを MCP ツール経由で取得できます。Claude をはじめとする LLM でのエネルギー使用状況の分析に活用できます。
5
+ 電力の使用量・コスト見積、契約情報、料金プランなどを MCP ツール経由で取得できます。Claude をはじめとする LLM でのエネルギー使用状況の分析に活用できます。
6
6
 
7
7
  ## 特徴
8
8
 
9
- - 電気・ガスの使用量データ取得
10
- - 料金プロダクト一覧・単価検索(Agile の30分単位価格にも対応)
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 API キーとアカウント番号
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
- OCTOPUS_API_KEY=<token> OCTOPUS_ACCOUNT_NUMBER=A-AAAA1111 npx tako-mcp
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
- OCTOPUS_API_KEY=<token> OCTOPUS_ACCOUNT_NUMBER=A-AAAA1111 npx tako-mcp --http
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
- | `OCTOPUS_API_KEY` | Yes | Octopus Energy API キー(Basic Auth のユーザー名として使用) |
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
- | `get_gas_consumption` | ガスの使用量データ(期間グルーピング対応) | 必要 |
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
- "OCTOPUS_API_KEY": "<your-api-key>",
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.octopus.energy/v1";
8
- var OctopusClient = class {
9
- apiKey;
10
- accountNumber;
11
- authHeader;
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
- async fetch(path, options) {
19
- const url = new URL(`${BASE_URL}${path}`);
20
- if (options?.params) {
21
- for (const [key, value] of Object.entries(options.params)) {
22
- if (value !== void 0 && value !== "") {
23
- url.searchParams.set(key, value);
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
- async fetchAllPages(path, options) {
41
- const results = [];
42
- let url = `${BASE_URL}${path}`;
43
- if (options?.params) {
44
- const u = new URL(url);
45
- for (const [key, value] of Object.entries(options.params)) {
46
- if (value !== void 0 && value !== "") {
47
- u.searchParams.set(key, value);
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
- async resolveMeterInfo() {
71
- if (this.meterInfoCache) {
72
- return this.meterInfoCache;
73
- }
74
- const account = await this.getAccount();
75
- const activeProperty = account.properties.find((p) => !p.moved_out_at) ?? account.properties[0];
76
- if (!activeProperty) {
77
- throw new Error("No properties found on account");
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
- async getGasConsumption(params) {
121
- const meterInfo = await this.resolveMeterInfo();
122
- if (!meterInfo.gas) {
123
- throw new Error("No gas meter found on account");
124
- }
125
- const { mprn, serialNumber } = meterInfo.gas;
126
- const queryParams = {
127
- period_from: params.period_from,
128
- period_to: params.period_to
129
- };
130
- if (params.group_by) {
131
- queryParams.group_by = params.group_by;
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 listProducts(filter) {
145
- const params = {};
146
- if (filter?.is_variable !== void 0) {
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
- if (filter?.is_business !== void 0) {
153
- params.is_business = String(filter.is_business);
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
- async getProduct(productCode, tariffsActiveAt) {
167
- const params = {};
168
- if (tariffsActiveAt) {
169
- params.tariffs_active_at = tariffsActiveAt;
170
- }
171
- return this.fetch(`/products/${productCode}/`, {
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
- async getElectricityRates(productCode, tariffCode, rateType = "standard", params) {
177
- const rateSegment = rateType === "standard" ? "standard-unit-rates" : `${rateType}-unit-rates`;
178
- const queryParams = {};
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
- if (params?.period_to) {
183
- queryParams.period_to = params.period_to;
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 (params?.page_size) {
186
- queryParams.page_size = String(params.page_size);
114
+ if (!json.data) {
115
+ throw new Error("No data in GraphQL response");
187
116
  }
188
- return this.fetchAllPages(
189
- `/products/${productCode}/electricity-tariffs/${tariffCode}/${rateSegment}/`,
190
- { auth: false, params: queryParams }
191
- );
117
+ return json.data;
192
118
  }
193
- async getGasRates(productCode, tariffCode, params) {
194
- const queryParams = {};
195
- if (params?.period_from) {
196
- queryParams.period_from = params.period_from;
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
- if (params?.period_to) {
199
- queryParams.period_to = params.period_to;
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 (params?.page_size) {
202
- queryParams.page_size = String(params.page_size);
139
+ if (!json.data) {
140
+ throw new Error("No data in GraphQL response");
203
141
  }
204
- return this.fetchAllPages(
205
- `/products/${productCode}/gas-tariffs/${tariffCode}/standard-unit-rates/`,
206
- { auth: false, params: queryParams }
207
- );
142
+ return json.data;
208
143
  }
209
- async getStandingCharges(productCode, tariffCode, fuel, params) {
210
- const fuelSegment = fuel === "electricity" ? "electricity-tariffs" : "gas-tariffs";
211
- const queryParams = {};
212
- if (params?.period_from) {
213
- queryParams.period_from = params.period_from;
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 getGridSupplyPoint(postcode) {
227
- const data = await this.fetch(
228
- "/industry/grid-supply-points/",
229
- {
230
- auth: false,
231
- params: { postcode }
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
- return data.results;
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.tool(
171
+ server.registerTool(
244
172
  "get_account_info",
245
- "Get meter information and tariff history for the account. Does not include address or personal details.",
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
- moved_in_at: p.moved_in_at,
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/consumption.ts
196
+ // src/tools/area.ts
268
197
  import { z } from "zod";
269
- var consumptionParams = {
270
- period_from: z.string().describe(
271
- "Start datetime in ISO 8601 format (UTC), e.g. 2024-01-01T00:00Z"
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
- product_code: z3.string().describe("The product code, e.g. AGILE-FLEX-22-11-25"),
364
- tariffs_active_at: z3.string().optional().describe("Only show tariffs active at this datetime (ISO 8601)")
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.getProduct(
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/rates.ts
381
- import { z as z4 } from "zod";
382
- function registerRateTools(server, client2) {
383
- server.tool(
384
- "get_electricity_rates",
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
- product_code: z4.string().describe("The product code"),
440
- tariff_code: z4.string().describe("The tariff code"),
441
- fuel: z4.enum(["electricity", "gas"]).describe("Fuel type"),
442
- period_from: z4.string().optional().describe("Start datetime (ISO 8601)"),
443
- period_to: z4.string().optional().describe("End datetime (ISO 8601)")
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.getStandingCharges(
447
- args.product_code,
448
- args.tariff_code,
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: "0.1.0"
252
+ version: "1.0.0"
469
253
  });
470
254
  registerAccountTools(server, client2);
471
255
  registerConsumptionTools(server, client2);
472
- registerProductTools(server, client2);
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 apiKey = process.env.OCTOPUS_API_KEY;
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 (!apiKey || !accountNumber) {
264
+ if (!email || !password || !accountNumber) {
482
265
  console.error(
483
- "Missing required environment variables: OCTOPUS_API_KEY, OCTOPUS_ACCOUNT_NUMBER"
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(apiKey, accountNumber);
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.2.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": [
@@ -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
- });