tako-mcp 0.1.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 +99 -0
- package/dist/get-credentials.js +89 -0
- package/dist/index.js +500 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# tako-mcp
|
|
2
|
+
|
|
3
|
+
[Octopus Energy API](https://developer.octopus.energy/docs/api/) の MCP サーバー。
|
|
4
|
+
|
|
5
|
+
電気・ガスの使用量、料金プラン、単価などを MCP ツール経由で取得できます。Claude をはじめとする LLM でのエネルギー使用状況の分析に活用できます。
|
|
6
|
+
|
|
7
|
+
## 特徴
|
|
8
|
+
|
|
9
|
+
- 電気・ガスの使用量データ取得
|
|
10
|
+
- 料金プロダクト一覧・単価検索(Agile の30分単位価格にも対応)
|
|
11
|
+
- 基本料金(スタンディングチャージ)の取得
|
|
12
|
+
- 郵便番号からのグリッド供給ポイント検索
|
|
13
|
+
- MPAN/MPRN/シリアル番号の自動解決(LLM コンテキストには露出しない)
|
|
14
|
+
- プライバシー重視: 住所等の個人情報はツールレスポンスから除外
|
|
15
|
+
|
|
16
|
+
## 必要条件
|
|
17
|
+
|
|
18
|
+
- 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` 環境変数で上書きしてください。
|
|
39
|
+
|
|
40
|
+
## 使い方
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
OCTOPUS_API_KEY=<token> OCTOPUS_ACCOUNT_NUMBER=A-AAAA1111 npx tako-mcp
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
デフォルトで `http://localhost:3000` で起動します。MCP エンドポイントは `/mcp` です。
|
|
47
|
+
|
|
48
|
+
### 環境変数
|
|
49
|
+
|
|
50
|
+
| 変数名 | 必須 | 説明 |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `OCTOPUS_API_KEY` | Yes | Octopus Energy API キー(Basic Auth のユーザー名として使用) |
|
|
53
|
+
| `OCTOPUS_ACCOUNT_NUMBER` | Yes | アカウント番号(例: `A-AAAA1111`) |
|
|
54
|
+
| `PORT` | No | HTTP サーバーのポート番号(デフォルト: `3000`) |
|
|
55
|
+
|
|
56
|
+
## MCP ツール一覧
|
|
57
|
+
|
|
58
|
+
| ツール | 説明 | 認証 |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `get_account_info` | メーター情報と料金プラン履歴(住所は除外) | 必要 |
|
|
61
|
+
| `get_electricity_consumption` | 電気の使用量データ(期間グルーピング対応) | 必要 |
|
|
62
|
+
| `get_gas_consumption` | ガスの使用量データ(期間グルーピング対応) | 必要 |
|
|
63
|
+
| `list_products` | 利用可能な料金プロダクト一覧 | 不要 |
|
|
64
|
+
| `get_product` | プロダクト詳細(地域別料金含む) | 不要 |
|
|
65
|
+
| `get_electricity_rates` | 電気料金の単位料金(standard/day/night) | 不要 |
|
|
66
|
+
| `get_gas_rates` | ガス料金の単位料金 | 不要 |
|
|
67
|
+
| `get_standing_charges` | 電気・ガスの基本料金 | 不要 |
|
|
68
|
+
| `get_grid_supply_point` | 郵便番号からグリッド供給ポイントを検索 | 不要 |
|
|
69
|
+
|
|
70
|
+
## クライアント設定
|
|
71
|
+
|
|
72
|
+
### Claude Code
|
|
73
|
+
|
|
74
|
+
MCP 設定に以下を追加:
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"mcpServers": {
|
|
79
|
+
"tako-mcp": {
|
|
80
|
+
"type": "http",
|
|
81
|
+
"url": "http://localhost:3000/mcp"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 開発
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npm install
|
|
91
|
+
npm run dev # tsx で起動
|
|
92
|
+
npm run build # tsup でバンドル
|
|
93
|
+
npm run typecheck # 型チェック
|
|
94
|
+
npm run check # Lint & フォーマット (Biome)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## ライセンス
|
|
98
|
+
|
|
99
|
+
MIT
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StreamableHTTPTransport } from "@hono/mcp";
|
|
5
|
+
import { serve } from "@hono/node-server";
|
|
6
|
+
import { Hono } from "hono";
|
|
7
|
+
|
|
8
|
+
// src/client.ts
|
|
9
|
+
var BASE_URL = "https://api.octopus.energy/v1";
|
|
10
|
+
var OctopusClient = class {
|
|
11
|
+
apiKey;
|
|
12
|
+
accountNumber;
|
|
13
|
+
authHeader;
|
|
14
|
+
meterInfoCache = null;
|
|
15
|
+
constructor(apiKey2, accountNumber2) {
|
|
16
|
+
this.apiKey = apiKey2;
|
|
17
|
+
this.accountNumber = accountNumber2;
|
|
18
|
+
this.authHeader = `Basic ${btoa(`${this.apiKey}:`)}`;
|
|
19
|
+
}
|
|
20
|
+
async fetch(path, options) {
|
|
21
|
+
const url = new URL(`${BASE_URL}${path}`);
|
|
22
|
+
if (options?.params) {
|
|
23
|
+
for (const [key, value] of Object.entries(options.params)) {
|
|
24
|
+
if (value !== void 0 && value !== "") {
|
|
25
|
+
url.searchParams.set(key, value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const headers = {};
|
|
30
|
+
if (options?.auth !== false) {
|
|
31
|
+
headers.Authorization = this.authHeader;
|
|
32
|
+
}
|
|
33
|
+
const response = await fetch(url.toString(), { headers });
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Octopus Energy API error: ${response.status} ${response.statusText}`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return response.json();
|
|
40
|
+
}
|
|
41
|
+
/** ページネーションを自動追従して全件取得 */
|
|
42
|
+
async fetchAllPages(path, options) {
|
|
43
|
+
const results = [];
|
|
44
|
+
let url = `${BASE_URL}${path}`;
|
|
45
|
+
if (options?.params) {
|
|
46
|
+
const u = new URL(url);
|
|
47
|
+
for (const [key, value] of Object.entries(options.params)) {
|
|
48
|
+
if (value !== void 0 && value !== "") {
|
|
49
|
+
u.searchParams.set(key, value);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
url = u.toString();
|
|
53
|
+
}
|
|
54
|
+
while (url) {
|
|
55
|
+
const headers = {};
|
|
56
|
+
if (options?.auth !== false) {
|
|
57
|
+
headers.Authorization = this.authHeader;
|
|
58
|
+
}
|
|
59
|
+
const response = await fetch(url, { headers });
|
|
60
|
+
if (!response.ok) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`Octopus Energy API error: ${response.status} ${response.statusText}`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
results.push(...data.results);
|
|
67
|
+
url = data.next;
|
|
68
|
+
}
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
/** アカウント情報からメーター情報を解決してキャッシュ */
|
|
72
|
+
async resolveMeterInfo() {
|
|
73
|
+
if (this.meterInfoCache) {
|
|
74
|
+
return this.meterInfoCache;
|
|
75
|
+
}
|
|
76
|
+
const account = await this.getAccount();
|
|
77
|
+
const activeProperty = account.properties.find((p) => !p.moved_out_at) ?? account.properties[0];
|
|
78
|
+
if (!activeProperty) {
|
|
79
|
+
throw new Error("No properties found on account");
|
|
80
|
+
}
|
|
81
|
+
const elecPoint = activeProperty.electricity_meter_points[0];
|
|
82
|
+
const gasPoint = activeProperty.gas_meter_points[0];
|
|
83
|
+
this.meterInfoCache = {
|
|
84
|
+
electricity: elecPoint ? {
|
|
85
|
+
mpan: elecPoint.mpan,
|
|
86
|
+
serialNumber: elecPoint.meters[0]?.serial_number ?? ""
|
|
87
|
+
} : null,
|
|
88
|
+
gas: gasPoint ? {
|
|
89
|
+
mprn: gasPoint.mprn,
|
|
90
|
+
serialNumber: gasPoint.meters[0]?.serial_number ?? ""
|
|
91
|
+
} : null
|
|
92
|
+
};
|
|
93
|
+
return this.meterInfoCache;
|
|
94
|
+
}
|
|
95
|
+
async getAccount() {
|
|
96
|
+
return this.fetch(`/accounts/${this.accountNumber}/`);
|
|
97
|
+
}
|
|
98
|
+
async getElectricityConsumption(params) {
|
|
99
|
+
const meterInfo = await this.resolveMeterInfo();
|
|
100
|
+
if (!meterInfo.electricity) {
|
|
101
|
+
throw new Error("No electricity meter found on account");
|
|
102
|
+
}
|
|
103
|
+
const { mpan, serialNumber } = meterInfo.electricity;
|
|
104
|
+
const queryParams = {
|
|
105
|
+
period_from: params.period_from,
|
|
106
|
+
period_to: params.period_to
|
|
107
|
+
};
|
|
108
|
+
if (params.group_by) {
|
|
109
|
+
queryParams.group_by = params.group_by;
|
|
110
|
+
}
|
|
111
|
+
if (params.order_by) {
|
|
112
|
+
queryParams.order_by = params.order_by;
|
|
113
|
+
}
|
|
114
|
+
if (params.page_size) {
|
|
115
|
+
queryParams.page_size = String(params.page_size);
|
|
116
|
+
}
|
|
117
|
+
return this.fetchAllPages(
|
|
118
|
+
`/electricity-meter-points/${mpan}/meters/${serialNumber}/consumption/`,
|
|
119
|
+
{ params: queryParams }
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
async getGasConsumption(params) {
|
|
123
|
+
const meterInfo = await this.resolveMeterInfo();
|
|
124
|
+
if (!meterInfo.gas) {
|
|
125
|
+
throw new Error("No gas meter found on account");
|
|
126
|
+
}
|
|
127
|
+
const { mprn, serialNumber } = meterInfo.gas;
|
|
128
|
+
const queryParams = {
|
|
129
|
+
period_from: params.period_from,
|
|
130
|
+
period_to: params.period_to
|
|
131
|
+
};
|
|
132
|
+
if (params.group_by) {
|
|
133
|
+
queryParams.group_by = params.group_by;
|
|
134
|
+
}
|
|
135
|
+
if (params.order_by) {
|
|
136
|
+
queryParams.order_by = params.order_by;
|
|
137
|
+
}
|
|
138
|
+
if (params.page_size) {
|
|
139
|
+
queryParams.page_size = String(params.page_size);
|
|
140
|
+
}
|
|
141
|
+
return this.fetchAllPages(
|
|
142
|
+
`/gas-meter-points/${mprn}/meters/${serialNumber}/consumption/`,
|
|
143
|
+
{ params: queryParams }
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
async listProducts(filter) {
|
|
147
|
+
const params = {};
|
|
148
|
+
if (filter?.is_variable !== void 0) {
|
|
149
|
+
params.is_variable = String(filter.is_variable);
|
|
150
|
+
}
|
|
151
|
+
if (filter?.is_green !== void 0) {
|
|
152
|
+
params.is_green = String(filter.is_green);
|
|
153
|
+
}
|
|
154
|
+
if (filter?.is_business !== void 0) {
|
|
155
|
+
params.is_business = String(filter.is_business);
|
|
156
|
+
}
|
|
157
|
+
if (filter?.available_at) {
|
|
158
|
+
params.available_at = filter.available_at;
|
|
159
|
+
}
|
|
160
|
+
if (filter?.is_prepay !== void 0) {
|
|
161
|
+
params.is_prepay = String(filter.is_prepay);
|
|
162
|
+
}
|
|
163
|
+
return this.fetchAllPages("/products/", {
|
|
164
|
+
auth: false,
|
|
165
|
+
params
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
async getProduct(productCode, tariffsActiveAt) {
|
|
169
|
+
const params = {};
|
|
170
|
+
if (tariffsActiveAt) {
|
|
171
|
+
params.tariffs_active_at = tariffsActiveAt;
|
|
172
|
+
}
|
|
173
|
+
return this.fetch(`/products/${productCode}/`, {
|
|
174
|
+
auth: false,
|
|
175
|
+
params
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async getElectricityRates(productCode, tariffCode, rateType = "standard", params) {
|
|
179
|
+
const rateSegment = rateType === "standard" ? "standard-unit-rates" : `${rateType}-unit-rates`;
|
|
180
|
+
const queryParams = {};
|
|
181
|
+
if (params?.period_from) {
|
|
182
|
+
queryParams.period_from = params.period_from;
|
|
183
|
+
}
|
|
184
|
+
if (params?.period_to) {
|
|
185
|
+
queryParams.period_to = params.period_to;
|
|
186
|
+
}
|
|
187
|
+
if (params?.page_size) {
|
|
188
|
+
queryParams.page_size = String(params.page_size);
|
|
189
|
+
}
|
|
190
|
+
return this.fetchAllPages(
|
|
191
|
+
`/products/${productCode}/electricity-tariffs/${tariffCode}/${rateSegment}/`,
|
|
192
|
+
{ auth: false, params: queryParams }
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
async getGasRates(productCode, tariffCode, params) {
|
|
196
|
+
const queryParams = {};
|
|
197
|
+
if (params?.period_from) {
|
|
198
|
+
queryParams.period_from = params.period_from;
|
|
199
|
+
}
|
|
200
|
+
if (params?.period_to) {
|
|
201
|
+
queryParams.period_to = params.period_to;
|
|
202
|
+
}
|
|
203
|
+
if (params?.page_size) {
|
|
204
|
+
queryParams.page_size = String(params.page_size);
|
|
205
|
+
}
|
|
206
|
+
return this.fetchAllPages(
|
|
207
|
+
`/products/${productCode}/gas-tariffs/${tariffCode}/standard-unit-rates/`,
|
|
208
|
+
{ auth: false, params: queryParams }
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
async getStandingCharges(productCode, tariffCode, fuel, params) {
|
|
212
|
+
const fuelSegment = fuel === "electricity" ? "electricity-tariffs" : "gas-tariffs";
|
|
213
|
+
const queryParams = {};
|
|
214
|
+
if (params?.period_from) {
|
|
215
|
+
queryParams.period_from = params.period_from;
|
|
216
|
+
}
|
|
217
|
+
if (params?.period_to) {
|
|
218
|
+
queryParams.period_to = params.period_to;
|
|
219
|
+
}
|
|
220
|
+
if (params?.page_size) {
|
|
221
|
+
queryParams.page_size = String(params.page_size);
|
|
222
|
+
}
|
|
223
|
+
return this.fetchAllPages(
|
|
224
|
+
`/products/${productCode}/${fuelSegment}/${tariffCode}/standing-charges/`,
|
|
225
|
+
{ auth: false, params: queryParams }
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
async getGridSupplyPoint(postcode) {
|
|
229
|
+
const data = await this.fetch(
|
|
230
|
+
"/industry/grid-supply-points/",
|
|
231
|
+
{
|
|
232
|
+
auth: false,
|
|
233
|
+
params: { postcode }
|
|
234
|
+
}
|
|
235
|
+
);
|
|
236
|
+
return data.results;
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// src/server.ts
|
|
241
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
242
|
+
|
|
243
|
+
// src/tools/account.ts
|
|
244
|
+
function registerAccountTools(server, client2) {
|
|
245
|
+
server.tool(
|
|
246
|
+
"get_account_info",
|
|
247
|
+
"Get meter information and tariff history for the account. Does not include address or personal details.",
|
|
248
|
+
async () => {
|
|
249
|
+
const account = await client2.getAccount();
|
|
250
|
+
const sanitized = {
|
|
251
|
+
number: account.number,
|
|
252
|
+
properties: account.properties.map((p) => ({
|
|
253
|
+
id: p.id,
|
|
254
|
+
moved_in_at: p.moved_in_at,
|
|
255
|
+
moved_out_at: p.moved_out_at,
|
|
256
|
+
electricity_meter_points: p.electricity_meter_points,
|
|
257
|
+
gas_meter_points: p.gas_meter_points
|
|
258
|
+
}))
|
|
259
|
+
};
|
|
260
|
+
return {
|
|
261
|
+
content: [
|
|
262
|
+
{ type: "text", text: JSON.stringify(sanitized, null, 2) }
|
|
263
|
+
]
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/tools/consumption.ts
|
|
270
|
+
import { z } from "zod";
|
|
271
|
+
var consumptionParams = {
|
|
272
|
+
period_from: z.string().describe(
|
|
273
|
+
"Start datetime in ISO 8601 format (UTC), e.g. 2024-01-01T00:00Z"
|
|
274
|
+
),
|
|
275
|
+
period_to: z.string().describe("End datetime in ISO 8601 format (UTC), e.g. 2024-02-01T00:00Z"),
|
|
276
|
+
group_by: z.enum(["day", "week", "month", "quarter"]).optional().describe("Group consumption data by time period")
|
|
277
|
+
};
|
|
278
|
+
function registerConsumptionTools(server, client2) {
|
|
279
|
+
server.tool(
|
|
280
|
+
"get_electricity_consumption",
|
|
281
|
+
"Get electricity consumption data for the account. MPAN and serial number are resolved automatically.",
|
|
282
|
+
consumptionParams,
|
|
283
|
+
async (args) => {
|
|
284
|
+
const data = await client2.getElectricityConsumption({
|
|
285
|
+
period_from: args.period_from,
|
|
286
|
+
period_to: args.period_to,
|
|
287
|
+
group_by: args.group_by
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
content: [
|
|
291
|
+
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
292
|
+
]
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
server.tool(
|
|
297
|
+
"get_gas_consumption",
|
|
298
|
+
"Get gas consumption data for the account. MPRN and serial number are resolved automatically.",
|
|
299
|
+
consumptionParams,
|
|
300
|
+
async (args) => {
|
|
301
|
+
const data = await client2.getGasConsumption({
|
|
302
|
+
period_from: args.period_from,
|
|
303
|
+
period_to: args.period_to,
|
|
304
|
+
group_by: args.group_by
|
|
305
|
+
});
|
|
306
|
+
return {
|
|
307
|
+
content: [
|
|
308
|
+
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
309
|
+
]
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/tools/grid.ts
|
|
316
|
+
import { z as z2 } from "zod";
|
|
317
|
+
function registerGridTools(server, client2) {
|
|
318
|
+
server.tool(
|
|
319
|
+
"get_grid_supply_point",
|
|
320
|
+
"Look up Grid Supply Point (GSP) information by postcode. Useful for determining the regional electricity distribution zone. No authentication required.",
|
|
321
|
+
{
|
|
322
|
+
postcode: z2.string().describe("UK postcode, e.g. SW1A 1AA")
|
|
323
|
+
},
|
|
324
|
+
async (args) => {
|
|
325
|
+
const data = await client2.getGridSupplyPoint(args.postcode);
|
|
326
|
+
return {
|
|
327
|
+
content: [
|
|
328
|
+
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
329
|
+
]
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/tools/products.ts
|
|
336
|
+
import { z as z3 } from "zod";
|
|
337
|
+
function registerProductTools(server, client2) {
|
|
338
|
+
server.tool(
|
|
339
|
+
"list_products",
|
|
340
|
+
"List available energy products/tariffs. No authentication required.",
|
|
341
|
+
{
|
|
342
|
+
is_variable: z3.boolean().optional().describe("Filter by variable tariffs"),
|
|
343
|
+
is_green: z3.boolean().optional().describe("Filter by green tariffs"),
|
|
344
|
+
is_business: z3.boolean().optional().describe("Filter by business tariffs"),
|
|
345
|
+
available_at: z3.string().optional().describe("Filter products available at this datetime (ISO 8601)")
|
|
346
|
+
},
|
|
347
|
+
async (args) => {
|
|
348
|
+
const data = await client2.listProducts({
|
|
349
|
+
is_variable: args.is_variable,
|
|
350
|
+
is_green: args.is_green,
|
|
351
|
+
is_business: args.is_business,
|
|
352
|
+
available_at: args.available_at
|
|
353
|
+
});
|
|
354
|
+
return {
|
|
355
|
+
content: [
|
|
356
|
+
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
357
|
+
]
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
);
|
|
361
|
+
server.tool(
|
|
362
|
+
"get_product",
|
|
363
|
+
"Get detailed information about a specific product, including regional tariff details. No authentication required.",
|
|
364
|
+
{
|
|
365
|
+
product_code: z3.string().describe("The product code, e.g. AGILE-FLEX-22-11-25"),
|
|
366
|
+
tariffs_active_at: z3.string().optional().describe("Only show tariffs active at this datetime (ISO 8601)")
|
|
367
|
+
},
|
|
368
|
+
async (args) => {
|
|
369
|
+
const data = await client2.getProduct(
|
|
370
|
+
args.product_code,
|
|
371
|
+
args.tariffs_active_at
|
|
372
|
+
);
|
|
373
|
+
return {
|
|
374
|
+
content: [
|
|
375
|
+
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
376
|
+
]
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/tools/rates.ts
|
|
383
|
+
import { z as z4 } from "zod";
|
|
384
|
+
function registerRateTools(server, client2) {
|
|
385
|
+
server.tool(
|
|
386
|
+
"get_electricity_rates",
|
|
387
|
+
"Get electricity unit rates for a product/tariff. Supports Agile's half-hourly pricing. No authentication required.",
|
|
388
|
+
{
|
|
389
|
+
product_code: z4.string().describe("The product code"),
|
|
390
|
+
tariff_code: z4.string().describe("The tariff code, e.g. E-1R-AGILE-FLEX-22-11-25-C"),
|
|
391
|
+
rate_type: z4.enum(["standard", "day", "night"]).optional().describe("Rate type (default: standard)"),
|
|
392
|
+
period_from: z4.string().optional().describe("Start datetime (ISO 8601)"),
|
|
393
|
+
period_to: z4.string().optional().describe("End datetime (ISO 8601)")
|
|
394
|
+
},
|
|
395
|
+
async (args) => {
|
|
396
|
+
const data = await client2.getElectricityRates(
|
|
397
|
+
args.product_code,
|
|
398
|
+
args.tariff_code,
|
|
399
|
+
args.rate_type ?? "standard",
|
|
400
|
+
{
|
|
401
|
+
period_from: args.period_from,
|
|
402
|
+
period_to: args.period_to
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
return {
|
|
406
|
+
content: [
|
|
407
|
+
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
408
|
+
]
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
server.tool(
|
|
413
|
+
"get_gas_rates",
|
|
414
|
+
"Get gas unit rates for a product/tariff. No authentication required.",
|
|
415
|
+
{
|
|
416
|
+
product_code: z4.string().describe("The product code"),
|
|
417
|
+
tariff_code: z4.string().describe("The tariff code, e.g. G-1R-VAR-17-01-11-D"),
|
|
418
|
+
period_from: z4.string().optional().describe("Start datetime (ISO 8601)"),
|
|
419
|
+
period_to: z4.string().optional().describe("End datetime (ISO 8601)")
|
|
420
|
+
},
|
|
421
|
+
async (args) => {
|
|
422
|
+
const data = await client2.getGasRates(
|
|
423
|
+
args.product_code,
|
|
424
|
+
args.tariff_code,
|
|
425
|
+
{
|
|
426
|
+
period_from: args.period_from,
|
|
427
|
+
period_to: args.period_to
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
return {
|
|
431
|
+
content: [
|
|
432
|
+
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
433
|
+
]
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
);
|
|
437
|
+
server.tool(
|
|
438
|
+
"get_standing_charges",
|
|
439
|
+
"Get standing charges (daily fixed charges) for a product/tariff. No authentication required.",
|
|
440
|
+
{
|
|
441
|
+
product_code: z4.string().describe("The product code"),
|
|
442
|
+
tariff_code: z4.string().describe("The tariff code"),
|
|
443
|
+
fuel: z4.enum(["electricity", "gas"]).describe("Fuel type"),
|
|
444
|
+
period_from: z4.string().optional().describe("Start datetime (ISO 8601)"),
|
|
445
|
+
period_to: z4.string().optional().describe("End datetime (ISO 8601)")
|
|
446
|
+
},
|
|
447
|
+
async (args) => {
|
|
448
|
+
const data = await client2.getStandingCharges(
|
|
449
|
+
args.product_code,
|
|
450
|
+
args.tariff_code,
|
|
451
|
+
args.fuel,
|
|
452
|
+
{
|
|
453
|
+
period_from: args.period_from,
|
|
454
|
+
period_to: args.period_to
|
|
455
|
+
}
|
|
456
|
+
);
|
|
457
|
+
return {
|
|
458
|
+
content: [
|
|
459
|
+
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
460
|
+
]
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/server.ts
|
|
467
|
+
function createMcpServer(client2) {
|
|
468
|
+
const server = new McpServer({
|
|
469
|
+
name: "tako-mcp",
|
|
470
|
+
version: "0.1.0"
|
|
471
|
+
});
|
|
472
|
+
registerAccountTools(server, client2);
|
|
473
|
+
registerConsumptionTools(server, client2);
|
|
474
|
+
registerProductTools(server, client2);
|
|
475
|
+
registerRateTools(server, client2);
|
|
476
|
+
registerGridTools(server, client2);
|
|
477
|
+
return server;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/index.ts
|
|
481
|
+
var apiKey = process.env.OCTOPUS_API_KEY;
|
|
482
|
+
var accountNumber = process.env.OCTOPUS_ACCOUNT_NUMBER;
|
|
483
|
+
if (!apiKey || !accountNumber) {
|
|
484
|
+
console.error(
|
|
485
|
+
"Missing required environment variables: OCTOPUS_API_KEY, OCTOPUS_ACCOUNT_NUMBER"
|
|
486
|
+
);
|
|
487
|
+
process.exit(1);
|
|
488
|
+
}
|
|
489
|
+
var port = Number(process.env.PORT) || 3e3;
|
|
490
|
+
var client = new OctopusClient(apiKey, accountNumber);
|
|
491
|
+
var mcpServer = createMcpServer(client);
|
|
492
|
+
var transport = new StreamableHTTPTransport();
|
|
493
|
+
await mcpServer.connect(transport);
|
|
494
|
+
var app = new Hono();
|
|
495
|
+
app.all("/mcp", (c) => transport.handleRequest(c));
|
|
496
|
+
app.get("/health", (c) => c.json({ status: "ok" }));
|
|
497
|
+
serve({ fetch: app.fetch, port }, () => {
|
|
498
|
+
console.log(`tako-mcp server running on http://localhost:${port}`);
|
|
499
|
+
console.log(`MCP endpoint: http://localhost:${port}/mcp`);
|
|
500
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tako-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for Octopus Energy API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tako-mcp": "dist/index.js",
|
|
8
|
+
"tako-mcp-get-credentials": "dist/get-credentials.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/arrow2nd/tako-mcp.git"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://github.com/arrow2nd/tako-mcp#readme",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/arrow2nd/tako-mcp/issues"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsup && chmod +x dist/*.js",
|
|
27
|
+
"prepublishOnly": "npm run build",
|
|
28
|
+
"dev": "tsx src/index.ts",
|
|
29
|
+
"check": "biome check",
|
|
30
|
+
"typecheck": "tsc --noEmit"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"mcp",
|
|
34
|
+
"octopus-energy"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"author": "arrow2nd",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@hono/mcp": "^0.2.3",
|
|
40
|
+
"@hono/node-server": "^1.19.9",
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
42
|
+
"hono": "^4.11.9",
|
|
43
|
+
"zod": "^4.3.6"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@biomejs/biome": "^2.3.14",
|
|
47
|
+
"@types/node": "^25.2.2",
|
|
48
|
+
"tsup": "^8.5.1",
|
|
49
|
+
"tsx": "^4.21.0",
|
|
50
|
+
"typescript": "^5.9.3"
|
|
51
|
+
}
|
|
52
|
+
}
|