kintai-plus-mcp 0.1.0 → 0.1.2
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 +14 -9
- package/dist/cli.mjs +137 -36
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
「出勤して」「今月の残業はどれくらい?」「昨日の工数を設計 2 時間・実装 5 時間で入れて」のように話しかけるだけで、打刻・勤務実績の確認・工数入力ができます。
|
|
9
9
|
|
|
10
10
|
- 手元の PC で動くローカルの stdio サーバーです(トークンを第三者のサーバーに預けません)
|
|
11
|
-
- トークンは OS
|
|
11
|
+
- トークンは OS のキーチェーンに保存し、有効期限が近づくと自動で再発行します(環境変数で渡したトークンは、明示しない限り保存も再発行もしません)
|
|
12
12
|
- 打刻の位置情報・IP アドレスは、既定では AI に渡しません
|
|
13
13
|
|
|
14
14
|
公開 API の仕様(公式): [マネーフォワード クラウド勤怠Plus 公開API 仕様](https://akashi.zendesk.com/hc/ja/articles/115000475854)
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
| `get_working_records` | 勤務実績(労働区分・時刻・実働・休憩。31 日ごとに自動で分割) |
|
|
27
27
|
| `summarize_working_hours` | 実働合計・出勤日数・休日出勤日数・法定時間外の目安(月ごとの内訳つき) |
|
|
28
28
|
| `get_alerts` | アラート(打刻忘れ、遅刻、36協定 など) |
|
|
29
|
-
| `check_attendance_issues` | 打刻漏れチェック(退勤・出勤・休憩戻の打刻忘れ、12 時間超の日、勤務間インターバル 11
|
|
29
|
+
| `check_attendance_issues` | 打刻漏れチェック(退勤・出勤・休憩戻の打刻忘れ、12 時間超の日、勤務間インターバル 11 時間未満、休日出勤)。勤務実績にその時刻がある日は打刻漏れにしない。休憩が不明な経過時間は、12 時間超を情報にとどめる |
|
|
30
30
|
| `list_projects` / `list_tasks` / `get_my_projects` | プロジェクトとタスク |
|
|
31
31
|
| `get_manhours` | 工数(プロジェクト名・タスク名つき) |
|
|
32
32
|
| `save_manhours` 【書き込み】 / `delete_manhours` 【書き込み】 | 工数の登録・更新・削除 |
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
|
|
64
64
|
**管理者が行うこと**(一度だけ): 従業員設定 > 対象の従業員の「変更」 > 利用機能設定 で、「公開API利用可否」を「利用する」にします。
|
|
65
65
|
|
|
66
|
-
**本人が行うこと**: マイページ > APIトークン で発行します。トークンの有効期限は 1 ヶ月と 1
|
|
66
|
+
**本人が行うこと**: マイページ > APIトークン で発行します。トークンの有効期限は 1 ヶ月と 1 日です。`login` で登録したトークンは、登録時と、期限が近づいたときに再発行します。
|
|
67
67
|
|
|
68
|
-
>
|
|
68
|
+
> `login` で登録するトークンは、このツール専用にしてください。再発行すると、それまで使っていたトークンは使えなくなります。環境変数で渡すだけの場合は、既定では再発行しません(下の「環境変数でトークンを渡す」)。
|
|
69
69
|
|
|
70
70
|
### 2. ログイン
|
|
71
71
|
|
|
@@ -165,24 +165,29 @@ args = ["-y", "kintai-plus-mcp"]
|
|
|
165
165
|
| 変数 | 既定 | 内容 |
|
|
166
166
|
|---|---|---|
|
|
167
167
|
| `KINTAI_PLUS_READ_ONLY` | `false` | `true` で書き込みツール(打刻・工数)を出さない |
|
|
168
|
-
| `
|
|
168
|
+
| `KINTAI_PLUS_CONFIRM_WRITES` | `false` | `true` で書き込みの前に確認ダイアログを出す(Elicitation に対応したクライアントのみ。未対応のクライアントでは書き込みが失敗します)。旧名の `CONFIRM_WRITES` も当面は読みます |
|
|
169
169
|
| `KINTAI_PLUS_INCLUDE_LOCATION` | `false` | `true` で打刻の位置情報・IP を返す |
|
|
170
170
|
| `KINTAI_PLUS_REFRESH_DAYS` | `15` | 有効期限の残りがこの日数を切ったら再発行する(1〜30) |
|
|
171
171
|
| `KINTAI_PLUS_AUTO_REFRESH` | `true` | `false` で自動の再発行を止める |
|
|
172
|
+
| `KINTAI_PLUS_PERSIST_ENV_TOKEN` | `false` | `true` で、環境変数のトークンを保存し、期限が不明なら再発行する。環境変数の値は使えなくなることがある |
|
|
172
173
|
| `KINTAI_PLUS_CREDENTIAL_STORE` | `auto` | 保存先: `auto`(キーチェーン → ファイル)/ `keychain` / `file` |
|
|
173
174
|
| `KINTAI_PLUS_CREDENTIALS_FILE` | `~/.config/kintai-plus-mcp/credentials.json` | ファイル保存のパス(指定するとファイル保存になる) |
|
|
174
175
|
| `KINTAI_PLUS_LOG_LEVEL` | `info` | `debug` / `info` / `warn` / `error` / `silent`(ログは stderr のみ) |
|
|
175
176
|
| `KINTAI_PLUS_MIN_INTERVAL_MS` | `300` | API リクエストの最小間隔(ミリ秒) |
|
|
176
177
|
|
|
177
|
-
###
|
|
178
|
+
### 環境変数でトークンを渡す(ボットなど)
|
|
178
179
|
|
|
179
|
-
|
|
180
|
+
`login` が使えないときは、環境変数だけで起動できます。**既定では保存せず、再発行 API も呼びません。**
|
|
180
181
|
|
|
181
182
|
```sh
|
|
182
183
|
KINTAI_PLUS_TOKEN=... KINTAI_PLUS_COMPANY_ID=... KINTAI_PLUS_HOST=atnd.ak4.jp npx kintai-plus-mcp
|
|
183
184
|
```
|
|
184
185
|
|
|
185
|
-
|
|
186
|
+
再発行 API は、渡したトークンを無効にして新しいトークンを返します。環境変数の値を自動で再発行すると、その値を共有している他のプロセスが突然使えなくなります。このサーバーは、明示しない限りそれをしません。
|
|
187
|
+
|
|
188
|
+
自動更新(期限が近づいたら再発行して保存)が必要なときは、このサーバー専用のトークンを `npx kintai-plus-mcp login` で登録してください。キーチェーンが使えない環境では、本人だけが読み書きできるファイル(パーミッション 600)に保存します。
|
|
189
|
+
|
|
190
|
+
環境変数のトークンを保存して再発行する動作は、`KINTAI_PLUS_PERSIST_ENV_TOKEN=true` を付けたときだけです。有効にすると、環境変数に書いたトークンは使えなくなることがあります。保存先にすでに認証情報があるときは、環境変数より保存先を優先します。
|
|
186
191
|
|
|
187
192
|
## 制約
|
|
188
193
|
|
|
@@ -227,7 +232,7 @@ This project is not affiliated with, endorsed by, or sponsored by Money Forward,
|
|
|
227
232
|
- Punch in/out, check today's status, view punch history, working records, alerts, and man-hours, and record man-hours
|
|
228
233
|
- Admin tools (staff list, team punches, working statuses, project members) are shown only to admins
|
|
229
234
|
- Runs locally over stdio: `npx kintai-plus-mcp`. Register your token first with `npx kintai-plus-mcp login`
|
|
230
|
-
- The token is stored in the OS keychain (or a 600-permission file) and reissued automatically before it expires
|
|
235
|
+
- The token is stored in the OS keychain (or a 600-permission file) and reissued automatically before it expires. A token passed only via `KINTAI_PLUS_TOKEN` is not stored or reissued unless `KINTAI_PLUS_PERSIST_ENV_TOKEN=true` (reissue invalidates the previous token)
|
|
231
236
|
- Location and IP address of punches are not sent to the AI by default
|
|
232
237
|
- Not supported: requests (申請) and schedules, because the public API does not provide them. Punches are recorded at the time the server receives them and cannot be edited via the API
|
|
233
238
|
- Requires Node.js 22+ and an API token issued from your Kintai Plus "My Page" (an admin must enable "公開API利用可否" for you)
|
package/dist/cli.mjs
CHANGED
|
@@ -621,6 +621,7 @@ var TokenManager = class {
|
|
|
621
621
|
this.#o = {
|
|
622
622
|
refreshBeforeDays: 15,
|
|
623
623
|
autoRefresh: true,
|
|
624
|
+
reissueUnknownExpiry: true,
|
|
624
625
|
now: () => /* @__PURE__ */ new Date(),
|
|
625
626
|
warn: () => {},
|
|
626
627
|
cacheTtlMs: 6e4,
|
|
@@ -653,7 +654,7 @@ var TokenManager = class {
|
|
|
653
654
|
return credentials;
|
|
654
655
|
}
|
|
655
656
|
#needsRefresh(credentials) {
|
|
656
|
-
if (credentials.expiresAt === void 0) return
|
|
657
|
+
if (credentials.expiresAt === void 0) return this.#o.reissueUnknownExpiry;
|
|
657
658
|
return Date.parse(credentials.expiresAt) - this.#o.now().getTime() <= this.#o.refreshBeforeDays * DAY_MS$1;
|
|
658
659
|
}
|
|
659
660
|
#isExpired(credentials) {
|
|
@@ -752,7 +753,7 @@ const silentLogger = createLogger({
|
|
|
752
753
|
// package.json
|
|
753
754
|
var name = "kintai-plus-mcp";
|
|
754
755
|
// src/version.ts
|
|
755
|
-
const VERSION = "0.1.
|
|
756
|
+
const VERSION = "0.1.2";
|
|
756
757
|
const PACKAGE_NAME = name;
|
|
757
758
|
// src/api/throttle.ts
|
|
758
759
|
const systemClock = {
|
|
@@ -906,10 +907,11 @@ function loadConfig(env = process.env) {
|
|
|
906
907
|
refreshBeforeDays: int(env, "KINTAI_PLUS_REFRESH_DAYS", 15, 1, 30),
|
|
907
908
|
autoRefresh: bool(env, "KINTAI_PLUS_AUTO_REFRESH", true),
|
|
908
909
|
readOnly: bool(env, "KINTAI_PLUS_READ_ONLY", false),
|
|
909
|
-
confirmWrites: bool(env, "CONFIRM_WRITES", false),
|
|
910
|
+
confirmWrites: read(env, "KINTAI_PLUS_CONFIRM_WRITES") !== void 0 ? bool(env, "KINTAI_PLUS_CONFIRM_WRITES", false) : bool(env, "CONFIRM_WRITES", false),
|
|
910
911
|
includeLocation: bool(env, "KINTAI_PLUS_INCLUDE_LOCATION", false),
|
|
911
912
|
logLevel: oneOf(env, "KINTAI_PLUS_LOG_LEVEL", LOG_LEVELS, "info"),
|
|
912
|
-
minIntervalMs: int(env, "KINTAI_PLUS_MIN_INTERVAL_MS", 300, 100, 6e4)
|
|
913
|
+
minIntervalMs: int(env, "KINTAI_PLUS_MIN_INTERVAL_MS", 300, 100, 6e4),
|
|
914
|
+
persistEnvToken: bool(env, "KINTAI_PLUS_PERSIST_ENV_TOKEN", false)
|
|
913
915
|
};
|
|
914
916
|
}
|
|
915
917
|
function read(env, name) {
|
|
@@ -1167,10 +1169,11 @@ async function selectCredentialStore(options = {}) {
|
|
|
1167
1169
|
return fallback;
|
|
1168
1170
|
}
|
|
1169
1171
|
/**
|
|
1170
|
-
*
|
|
1171
|
-
*
|
|
1172
|
+
* 保存先が空のとき、環境変数から認証情報を読む。
|
|
1173
|
+
* 既定では保存しない。`persist: true` のときだけ保存し、以降は保存先を正とする。
|
|
1174
|
+
* 保存先に既にあれば、環境変数は無視して保存先を返す。
|
|
1172
1175
|
*/
|
|
1173
|
-
async function importFromEnv(store, env = process.env, now = /* @__PURE__ */ new Date()) {
|
|
1176
|
+
async function importFromEnv(store, env = process.env, now = /* @__PURE__ */ new Date(), options = {}) {
|
|
1174
1177
|
const existing = await store.load();
|
|
1175
1178
|
if (existing) return existing;
|
|
1176
1179
|
const token = env[TOKEN_ENV]?.trim();
|
|
@@ -1186,9 +1189,41 @@ async function importFromEnv(store, env = process.env, now = /* @__PURE__ */ new
|
|
|
1186
1189
|
token,
|
|
1187
1190
|
updatedAt: now.toISOString()
|
|
1188
1191
|
};
|
|
1189
|
-
await store.save(credentials);
|
|
1192
|
+
if (options.persist) await store.save(credentials);
|
|
1190
1193
|
return credentials;
|
|
1191
1194
|
}
|
|
1195
|
+
/**
|
|
1196
|
+
* 保存先が空のあいだ、環境変数のトークンをメモリ上だけで返す。
|
|
1197
|
+
* 保存先に認証情報が入ったあと(login など)は保存先を読む。書き出しは保存先に既にあるときだけ。
|
|
1198
|
+
*/
|
|
1199
|
+
var EnvFallbackStore = class {
|
|
1200
|
+
#inner;
|
|
1201
|
+
#fallback;
|
|
1202
|
+
constructor(inner, fallback) {
|
|
1203
|
+
this.#inner = inner;
|
|
1204
|
+
this.#fallback = fallback;
|
|
1205
|
+
}
|
|
1206
|
+
get kind() {
|
|
1207
|
+
return this.#inner.kind;
|
|
1208
|
+
}
|
|
1209
|
+
get location() {
|
|
1210
|
+
return this.#inner.location;
|
|
1211
|
+
}
|
|
1212
|
+
async load() {
|
|
1213
|
+
return await this.#inner.load() ?? this.#fallback;
|
|
1214
|
+
}
|
|
1215
|
+
async save(credentials) {
|
|
1216
|
+
if (await this.#inner.load()) {
|
|
1217
|
+
await this.#inner.save(credentials);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
this.#fallback = credentials;
|
|
1221
|
+
}
|
|
1222
|
+
async clear() {
|
|
1223
|
+
this.#fallback = void 0;
|
|
1224
|
+
return this.#inner.clear();
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1192
1227
|
// src/runtime.ts
|
|
1193
1228
|
/** 設定・ログ・保存先・トークン管理・API クライアントを組み立てる。 */
|
|
1194
1229
|
async function createRuntime(options = {}) {
|
|
@@ -1213,10 +1248,25 @@ async function createRuntime(options = {}) {
|
|
|
1213
1248
|
...options.onResponse ? { onResponse: options.onResponse } : {}
|
|
1214
1249
|
});
|
|
1215
1250
|
const lockPath = join(dirname(defaultCredentialsPath(env)), "refresh.lock");
|
|
1251
|
+
let tokenStore = store;
|
|
1252
|
+
let usingEnvToken = false;
|
|
1253
|
+
try {
|
|
1254
|
+
const before = await store.load();
|
|
1255
|
+
const imported = await importFromEnv(store, env, options.now?.() ?? /* @__PURE__ */ new Date(), { persist: config.persistEnvToken });
|
|
1256
|
+
if (imported) logger.addSecret(imported.token);
|
|
1257
|
+
if (!before && imported && !config.persistEnvToken) {
|
|
1258
|
+
usingEnvToken = true;
|
|
1259
|
+
tokenStore = new EnvFallbackStore(store, imported);
|
|
1260
|
+
}
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
if (!(error instanceof CredentialsFormatError)) throw error;
|
|
1263
|
+
logger.warn(error.message);
|
|
1264
|
+
}
|
|
1216
1265
|
const tokens = new TokenManager({
|
|
1217
|
-
store,
|
|
1266
|
+
store: tokenStore,
|
|
1218
1267
|
refreshBeforeDays: config.refreshBeforeDays,
|
|
1219
1268
|
autoRefresh: config.autoRefresh,
|
|
1269
|
+
reissueUnknownExpiry: !usingEnvToken,
|
|
1220
1270
|
warn: (m) => logger.warn(m),
|
|
1221
1271
|
acquireLock: () => acquireFileLock(lockPath),
|
|
1222
1272
|
reissue: async (credentials) => {
|
|
@@ -1228,18 +1278,12 @@ async function createRuntime(options = {}) {
|
|
|
1228
1278
|
},
|
|
1229
1279
|
...options.now ? { now: options.now } : {}
|
|
1230
1280
|
});
|
|
1231
|
-
try {
|
|
1232
|
-
const imported = await importFromEnv(store, env, options.now?.() ?? /* @__PURE__ */ new Date());
|
|
1233
|
-
if (imported) logger.addSecret(imported.token);
|
|
1234
|
-
} catch (error) {
|
|
1235
|
-
if (!(error instanceof CredentialsFormatError)) throw error;
|
|
1236
|
-
logger.warn(error.message);
|
|
1237
|
-
}
|
|
1238
1281
|
return {
|
|
1239
1282
|
config,
|
|
1240
1283
|
logger,
|
|
1241
1284
|
store,
|
|
1242
1285
|
tokens,
|
|
1286
|
+
usingEnvToken,
|
|
1243
1287
|
createClient,
|
|
1244
1288
|
async getClient() {
|
|
1245
1289
|
const credentials = await tokens.getCredentials();
|
|
@@ -1389,20 +1433,28 @@ async function logout(deps) {
|
|
|
1389
1433
|
const removed = await runtime.store.clear();
|
|
1390
1434
|
io.stdout(removed ? `認証情報を削除しました: ${runtime.store.location}\nトークン自体はまだ有効です。不要ならマイページで無効化してください。
|
|
1391
1435
|
` : `保存された認証情報はありません: ${runtime.store.location}\n`);
|
|
1392
|
-
if (deps.runtimeOptions?.env
|
|
1436
|
+
if ((deps.runtimeOptions?.env ?? process.env).KINTAI_PLUS_TOKEN) io.stderr(runtime.config.persistEnvToken ? "注意: KINTAI_PLUS_TOKEN が設定されているため、次回の起動時に再び保存されます。再発行すると、環境変数の値は使えなくなることがあります。\n" : "注意: KINTAI_PLUS_TOKEN が設定されています。保存した認証情報が無くても、次回の起動はこの環境変数のトークンを使います。保存しません。再発行もしません。\n");
|
|
1393
1437
|
return 0;
|
|
1394
1438
|
}
|
|
1395
1439
|
async function status(deps) {
|
|
1396
1440
|
const { io } = deps;
|
|
1397
1441
|
const runtime = await createRuntime(deps.runtimeOptions);
|
|
1398
1442
|
const now = deps.now?.() ?? /* @__PURE__ */ new Date();
|
|
1399
|
-
const
|
|
1400
|
-
const
|
|
1443
|
+
const stored = await runtime.store.load();
|
|
1444
|
+
const fromEnv = !stored && runtime.usingEnvToken;
|
|
1445
|
+
const credentials = stored ?? (fromEnv ? await runtime.tokens.getCredentials() : void 0);
|
|
1401
1446
|
if (!credentials) {
|
|
1402
|
-
io.stdout(
|
|
1447
|
+
io.stdout(`保存先: ${runtime.store.location}\n未ログインです。\`npx kintai-plus-mcp login\` で登録してください。\n`);
|
|
1403
1448
|
return 1;
|
|
1404
1449
|
}
|
|
1405
|
-
lines
|
|
1450
|
+
const lines = [
|
|
1451
|
+
fromEnv ? "保存先: なし(環境変数のトークンを、このプロセスの中だけで使います)" : `保存先: ${runtime.store.location}`,
|
|
1452
|
+
`接続先: ${credentials.host}`,
|
|
1453
|
+
`ログイン企業ID: ${credentials.companyId}`,
|
|
1454
|
+
`従業員ID: ${credentials.staffId ?? "不明"}`,
|
|
1455
|
+
`有効期限: ${fromEnv ? "不明(環境変数のトークンは自動では再発行しません)" : formatExpiry(credentials.expiresAt, now)}`,
|
|
1456
|
+
`自動更新: ${fromEnv ? "無効(環境変数のトークンは再発行しません。自動更新が必要なら login で登録してください)" : runtime.config.autoRefresh ? `有効(残り ${runtime.config.refreshBeforeDays} 日を切ったら再発行)` : "無効"}`
|
|
1457
|
+
];
|
|
1406
1458
|
let code = 0;
|
|
1407
1459
|
try {
|
|
1408
1460
|
const staff = await fetchMyStaff(await runtime.getClient(), { staffId: credentials.staffId });
|
|
@@ -2512,7 +2564,9 @@ function apiHint(error) {
|
|
|
2512
2564
|
case "unknown_company": return "The company ID (ログイン企業ID) was not found. Check the company ID and that the host matches the domain after login (atnd.ak4.jp or atnd-awj.ak4.jp), then run `npx kintai-plus-mcp login` again.";
|
|
2513
2565
|
case "forbidden": return "Kintai Plus denied this operation. The user may not have permission for it (admin rights may be required), or the public API (公開API利用可否) may be disabled for this user.";
|
|
2514
2566
|
case "not_found": return "The requested item was not found. Check the IDs (staff, project, task) and the date.";
|
|
2515
|
-
case "invalid_request":
|
|
2567
|
+
case "invalid_request":
|
|
2568
|
+
if (error.apiErrors.some((e) => e.message?.includes("従業員IDは必ず入力してください"))) return "Kintai Plus also returns 「従業員IDは必ず入力してください」 when a staff ID does not exist or is outside the staff you manage. It does not always mean staffIds was omitted. Check the IDs with list_staff.";
|
|
2569
|
+
return "Kintai Plus rejected the request. Check the parameters (dates, IDs, period length).";
|
|
2516
2570
|
case "rate_limited": return "Too many requests. Wait a minute before trying again.";
|
|
2517
2571
|
case "server":
|
|
2518
2572
|
case "network":
|
|
@@ -2522,6 +2576,11 @@ function apiHint(error) {
|
|
|
2522
2576
|
}
|
|
2523
2577
|
// src/tools/common.ts
|
|
2524
2578
|
const dateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use YYYY-MM-DD").describe("Date in YYYY-MM-DD (Japan time)");
|
|
2579
|
+
/** 月。01–12 以外(2026-13 など)は拒否する */
|
|
2580
|
+
const monthSchema = z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/, "Use YYYY-MM (month 01-12)");
|
|
2581
|
+
function assertMonth(month) {
|
|
2582
|
+
if (!monthSchema.safeParse(month).success) throw new ToolInputError(`Invalid month: ${month} (use YYYY-MM, month 01-12)`);
|
|
2583
|
+
}
|
|
2525
2584
|
/** ツール入力の期間を検証して DateRange にする */
|
|
2526
2585
|
function resolveRange(input, now, defaults) {
|
|
2527
2586
|
const from = input.from ?? defaults.from ?? todayInTokyo(now);
|
|
@@ -2542,9 +2601,12 @@ function formatMinutes(minutes) {
|
|
|
2542
2601
|
return `${sign}${Math.floor(abs / 60)}:${String(abs % 60).padStart(2, "0")}`;
|
|
2543
2602
|
}
|
|
2544
2603
|
// src/tools/registry.ts
|
|
2545
|
-
/**
|
|
2604
|
+
/** 型推論のためのヘルパー。未知の引数は捨てずに拒否する */
|
|
2546
2605
|
function defineTool(tool) {
|
|
2547
|
-
return
|
|
2606
|
+
return {
|
|
2607
|
+
...tool,
|
|
2608
|
+
inputSchema: tool.inputSchema.strict()
|
|
2609
|
+
};
|
|
2548
2610
|
}
|
|
2549
2611
|
/** 権限・バックエンドの機能・読み取り専用モードに応じて、公開するツールを選ぶ(ADR 0004, 0005) */
|
|
2550
2612
|
function selectTools(tools, options) {
|
|
@@ -2649,7 +2711,21 @@ async function staffIndex(ctx) {
|
|
|
2649
2711
|
}
|
|
2650
2712
|
}
|
|
2651
2713
|
async function resolveStaffIds(input, ctx) {
|
|
2652
|
-
if (input)
|
|
2714
|
+
if (input) {
|
|
2715
|
+
const unique = [...new Set(input)];
|
|
2716
|
+
try {
|
|
2717
|
+
const listed = await ctx.backend.listStaff();
|
|
2718
|
+
if (!listed.truncated) {
|
|
2719
|
+
const known = new Set(listed.staff.map((s) => s.staffId));
|
|
2720
|
+
const unknown = unique.filter((id) => !known.has(id));
|
|
2721
|
+
if (unknown.length > 0) throw new ToolInputError(`Unknown staff ID(s): ${unknown.join(", ")}. They are not in the staff you manage. Use list_staff to get valid IDs. Kintai Plus reports an unknown ID as 「従業員IDは必ず入力してください」, which does not mean staffIds was omitted.`);
|
|
2722
|
+
}
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
if (error instanceof ToolInputError) throw error;
|
|
2725
|
+
ctx.logger.warn(`Could not check staff IDs (${error instanceof Error ? error.name : "error"})`);
|
|
2726
|
+
}
|
|
2727
|
+
return unique;
|
|
2728
|
+
}
|
|
2653
2729
|
const ids = (await managedStaff(ctx)).map((s) => s.staffId);
|
|
2654
2730
|
if (ids.length > 500) throw new ToolInputError(`You manage ${ids.length} staff; pass staffIds (up to 500) to narrow down.`);
|
|
2655
2731
|
return ids;
|
|
@@ -2795,11 +2871,12 @@ const getAlertsTool = defineTool({
|
|
|
2795
2871
|
name: "get_alerts",
|
|
2796
2872
|
title: "Get my attendance alerts",
|
|
2797
2873
|
description: "Get your own attendance alerts (アラート) from Kintai Plus, such as missing punches (打刻忘れ), suspected absence (欠勤疑い), late arrival (遅刻), early leave (早退), overtime threshold (残業時間閾値超え), 36 agreement (36協定), and missing man-hours (工数未入力). Optionally filter by month.",
|
|
2798
|
-
inputSchema: z.object({ month:
|
|
2874
|
+
inputSchema: z.object({ month: monthSchema.optional().describe("Only alerts for this month (YYYY-MM).") }),
|
|
2799
2875
|
capability: "alerts.read",
|
|
2800
2876
|
audience: "everyone",
|
|
2801
2877
|
kind: "read",
|
|
2802
2878
|
handler: async (input, ctx) => {
|
|
2879
|
+
if (input.month) assertMonth(input.month);
|
|
2803
2880
|
const all = await ctx.backend.getAlerts();
|
|
2804
2881
|
const alerts = input.month ? all.filter((a) => (a.month ?? a.date?.slice(0, 7)) === input.month) : all;
|
|
2805
2882
|
return {
|
|
@@ -2832,15 +2909,21 @@ const isOut = (s) => s.type === "clock_out" || s.type === "direct_out";
|
|
|
2832
2909
|
* 日をまたぐ勤務(前日に出勤し、翌日の最初の打刻が退勤)は 1 つの勤務として扱い、打刻漏れにしない。
|
|
2833
2910
|
* 範囲の初日の朝に終わる夜勤のため、stamps には範囲の前日の分も含めてよい。
|
|
2834
2911
|
* 休暇・申請は API で見えないため、「労働日に打刻なし」は休暇の可能性を添えて warning にとどめる。
|
|
2912
|
+
* 勤務実績に開始・終了(または出勤・退勤の打刻時刻)がある日は、打刻修正などで打刻が欠けていても打刻漏れにしない。
|
|
2913
|
+
* 休憩が不明な経過時間は 12 時間超を info にし、1 時間引いても超えるときだけ warning にする。
|
|
2835
2914
|
*/
|
|
2836
2915
|
function findAttendanceIssues(input) {
|
|
2837
2916
|
const issues = [];
|
|
2838
|
-
const add = (date, type, message) => issues.push({
|
|
2917
|
+
const add = (date, type, message, severity = SEVERITY[type]) => issues.push({
|
|
2839
2918
|
date,
|
|
2840
2919
|
type,
|
|
2841
|
-
severity
|
|
2920
|
+
severity,
|
|
2842
2921
|
message
|
|
2843
2922
|
});
|
|
2923
|
+
const reportOpenPunch = (open) => {
|
|
2924
|
+
if (recordHasClockOut(recordByDate.get(open.date))) return;
|
|
2925
|
+
add(open.date, open.type, open.type === "missing_clock_out" ? "退勤の打刻がありません(打刻忘れの可能性)" : "休憩戻の打刻がありません(休憩中のまま)");
|
|
2926
|
+
};
|
|
2844
2927
|
const stampsByDate = /* @__PURE__ */ new Map();
|
|
2845
2928
|
for (const stamp of [...input.stamps].sort((a, b) => a.stampedAt.getTime() - b.stampedAt.getTime())) {
|
|
2846
2929
|
const date = toLocalDate(stamp.stampedAt);
|
|
@@ -2858,9 +2941,9 @@ function findAttendanceIssues(input) {
|
|
|
2858
2941
|
const firstInOrOut = stamps.find((s) => isIn(s) || isOut(s));
|
|
2859
2942
|
const startsWithOut = firstInOrOut !== void 0 && isOut(firstInOrOut);
|
|
2860
2943
|
const overnight = pending?.type === "missing_clock_out" && pending.lastInAt !== void 0 && diffDays(pending.date, date) === 1 && startsWithOut && firstInOrOut.stampedAt.getTime() - pending.lastInAt.getTime() <= MAX_OVERNIGHT_SHIFT_MINUTES * 6e4;
|
|
2861
|
-
if (pending && !overnight)
|
|
2944
|
+
if (pending && !overnight) reportOpenPunch(pending);
|
|
2862
2945
|
pending = void 0;
|
|
2863
|
-
if (startsWithOut && !overnight) add(date, "missing_clock_in", "出勤の打刻がないまま退勤しています");
|
|
2946
|
+
if (startsWithOut && !overnight && !recordHasClockIn(record)) add(date, "missing_clock_in", "出勤の打刻がないまま退勤しています");
|
|
2864
2947
|
const status = computeDayStatus(stamps, endOfDay(date));
|
|
2865
2948
|
const lastIn = [...stamps].reverse().find(isIn);
|
|
2866
2949
|
if (status.state === "working") pending = {
|
|
@@ -2873,9 +2956,8 @@ function findAttendanceIssues(input) {
|
|
|
2873
2956
|
type: "missing_break_end"
|
|
2874
2957
|
};
|
|
2875
2958
|
const minutes = record?.actualWorkingMinutes ?? 0;
|
|
2876
|
-
const worked = record?.workingMinutesAreGross ? "開始から終了までの経過(休憩は不明)" : "実働";
|
|
2877
2959
|
if (record?.dayCategory === "workday" && stamps.length === 0 && minutes === 0 && !overnight) add(date, "no_punches_on_workday", "労働日ですが打刻がありません(休暇などの場合は問題ありません)");
|
|
2878
|
-
if (minutes > LONG_DAY_MINUTES)
|
|
2960
|
+
if (minutes > LONG_DAY_MINUTES) addLongDay(date, minutes, record?.workingMinutesAreGross === true, add);
|
|
2879
2961
|
const firstIn = stamps.find(isIn);
|
|
2880
2962
|
if (previousOut && firstIn && diffDays(previousOut.date, date) === 1) {
|
|
2881
2963
|
const gap = Math.round((firstIn.stampedAt.getTime() - previousOut.at.getTime()) / 6e4);
|
|
@@ -2889,9 +2971,27 @@ function findAttendanceIssues(input) {
|
|
|
2889
2971
|
if (minutes > 0 && record?.dayCategory === "legal_holiday") add(date, "holiday_work", holidayWorkMessage("法定休日", minutes, record.workingMinutesAreGross));
|
|
2890
2972
|
else if (minutes > 0 && record?.dayCategory === "scheduled_holiday") add(date, "holiday_work", holidayWorkMessage("所定休日", minutes, record.workingMinutesAreGross));
|
|
2891
2973
|
}
|
|
2892
|
-
if (pending)
|
|
2974
|
+
if (pending) reportOpenPunch(pending);
|
|
2893
2975
|
return issues.filter((issue) => issue.date >= input.range.from).sort((a, b) => a.date < b.date ? -1 : a.date > b.date ? 1 : 0);
|
|
2894
2976
|
}
|
|
2977
|
+
function recordHasClockIn(record) {
|
|
2978
|
+
return record?.startTime !== void 0 || record?.clockInAt !== void 0;
|
|
2979
|
+
}
|
|
2980
|
+
function recordHasClockOut(record) {
|
|
2981
|
+
return record?.endTime !== void 0 || record?.clockOutAt !== void 0;
|
|
2982
|
+
}
|
|
2983
|
+
function addLongDay(date, minutes, gross, add) {
|
|
2984
|
+
if (!gross) {
|
|
2985
|
+
add(date, "long_working_day", `実働が ${formatHm(minutes)} で、12 時間を超えています`);
|
|
2986
|
+
return;
|
|
2987
|
+
}
|
|
2988
|
+
const elapsed = `開始から終了までの経過が ${formatHm(minutes)}`;
|
|
2989
|
+
if (minutes > 780) {
|
|
2990
|
+
add(date, "long_working_day", `${elapsed} です。1 時間の休憩を差し引いても 12 時間を超えます(実際の休憩時間は不明)`);
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
add(date, "long_working_day", `${elapsed} で 12 時間を超えています。休憩時間が不明なため、実働はこれより短い可能性があります`, "info");
|
|
2994
|
+
}
|
|
2895
2995
|
function holidayWorkMessage(kind, minutes, gross) {
|
|
2896
2996
|
const base = `${kind}に ${formatHm(minutes)} 働いています`;
|
|
2897
2997
|
return gross ? `${base}(開始から終了までの経過。休憩は不明)` : base;
|
|
@@ -2904,7 +3004,7 @@ const MAX_DAYS = 93;
|
|
|
2904
3004
|
const checkAttendanceIssuesTool = defineTool({
|
|
2905
3005
|
name: "check_attendance_issues",
|
|
2906
3006
|
title: "Check my attendance for issues",
|
|
2907
|
-
description: `Check your own punches and working records for issues (打刻漏れチェック): missing clock out/in (退勤・出勤の打刻忘れ), break not ended (休憩戻なし), no punches on a workday, days over 12 hours, rest interval under 11 hours (勤務間インターバル), and holiday work. Defaults to the 1st of this month through yesterday (on the 1st, the previous month). Max ${MAX_DAYS} days. Overnight shifts are recognized. Leave and requests are not visible via the API, so "no punches" may simply be a day off.`,
|
|
3007
|
+
description: `Check your own punches and working records for issues (打刻漏れチェック): missing clock out/in (退勤・出勤の打刻忘れ), break not ended (休憩戻なし), no punches on a workday, days over 12 hours, rest interval under 11 hours (勤務間インターバル), and holiday work. A missing punch is not reported when the working record already has that start or end time (for example after a correction in the web UI). When break time is unknown, elapsed time over 12 hours is info unless it still exceeds 12 hours after a 1-hour break. Defaults to the 1st of this month through yesterday (on the 1st, the previous month). Max ${MAX_DAYS} days. Overnight shifts are recognized. Leave and requests are not visible via the API, so "no punches" may simply be a day off.`,
|
|
2908
3008
|
inputSchema: z.object({
|
|
2909
3009
|
from: dateSchema.optional().describe("Start date (YYYY-MM-DD)."),
|
|
2910
3010
|
to: dateSchema.optional().describe("End date, inclusive (YYYY-MM-DD). Defaults to yesterday.")
|
|
@@ -2959,7 +3059,7 @@ const saveManhoursTool = defineTool({
|
|
|
2959
3059
|
description: "Create or update your own man-hours (工数) for one day: minutes per project task. Only the listed tasks are overwritten; other tasks on that day are kept. To remove an entry, use delete_manhours. Use list_projects / get_my_projects and list_tasks to find IDs. Days that are closed (締め) or fixed (確定) cannot be changed.",
|
|
2960
3060
|
inputSchema: z.object({
|
|
2961
3061
|
date: dateSchema.describe("Date (YYYY-MM-DD)."),
|
|
2962
|
-
entries: z.array(z.
|
|
3062
|
+
entries: z.array(z.strictObject({
|
|
2963
3063
|
projectId: z.number().int().positive(),
|
|
2964
3064
|
taskId: z.number().int().positive(),
|
|
2965
3065
|
minutes: z.number().int().min(1).max(DAY_MINUTES).describe("Minutes worked on this task (e.g. 90 = 1.5 hours).")
|
|
@@ -3156,7 +3256,7 @@ const findOvertimeRisksTool = defineTool({
|
|
|
3156
3256
|
title: "Find overtime risks in my team",
|
|
3157
3257
|
description: "Admin only. For staff you manage, estimate statutory overtime (法定時間外の目安) so far this month and project it to the month end (月末の着地予測), then list who is over or likely to exceed the threshold (default 45 hours, the standard monthly limit under a 36 agreement). Levels: over (already exceeded), likely (projected to exceed), watch (projected ≥ 80%). Always present results as estimates.",
|
|
3158
3258
|
inputSchema: z.object({
|
|
3159
|
-
month:
|
|
3259
|
+
month: monthSchema.optional().describe("Month (YYYY-MM). Defaults to the current month."),
|
|
3160
3260
|
staffIds: z.array(z.number().int().positive()).min(1).max(500).optional().describe("Defaults to all staff you manage."),
|
|
3161
3261
|
thresholdHours: z.number().int().min(1).max(200).optional().describe("Monthly overtime threshold in hours. Default 45."),
|
|
3162
3262
|
includeAll: z.boolean().optional().describe("Include staff with no risk (level ok). Default false.")
|
|
@@ -3166,6 +3266,7 @@ const findOvertimeRisksTool = defineTool({
|
|
|
3166
3266
|
kind: "read",
|
|
3167
3267
|
handler: async (input, ctx) => {
|
|
3168
3268
|
const today = todayInTokyo(ctx.now());
|
|
3269
|
+
if (input.month) assertMonth(input.month);
|
|
3169
3270
|
const month = input.month ?? today.slice(0, 7);
|
|
3170
3271
|
const monthStart = `${month}-01`;
|
|
3171
3272
|
if (monthStart > today) throw new ToolInputError(`${month} has not started yet.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kintai-plus-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Unofficial MCP server for マネーフォワード クラウド勤怠Plus (formerly AKASHI) public API",
|
|
5
5
|
"mcpName": "io.github.atzzCokeK/kintai-plus-mcp",
|
|
6
6
|
"keywords": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"type": "module",
|
|
21
21
|
"bin": {
|
|
22
|
-
"kintai-plus-mcp": "
|
|
22
|
+
"kintai-plus-mcp": "dist/cli.mjs"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"dist"
|