@jurislm/judicial-mcp 1.3.0 → 1.6.3
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 +68 -9
- package/bin/judicial-mcp.ts +2 -0
- package/package.json +30 -17
- package/src/index.ts +20 -0
- package/src/public-judgment-source.ts +487 -0
- package/src/regulation-cache.ts +38 -0
- package/src/regulation-source.ts +536 -0
- package/src/response.ts +74 -0
- package/src/server.ts +70 -0
- package/src/source-error.ts +50 -0
- package/src/tools.ts +409 -0
- package/types/index.d.ts +1 -113
- package/types/public-judgment-source.d.ts +2 -0
- package/types/regulation-cache.d.ts +8 -0
- package/types/regulation-source.d.ts +29 -0
- package/types/response.d.ts +20 -0
- package/types/server.d.ts +3 -0
- package/types/source-error.d.ts +14 -0
- package/types/tools.d.ts +236 -0
- package/bin/judicial-mcp.js +0 -18
- package/src/index.js +0 -76
- package/src/response.js +0 -44
- package/src/tools.js +0 -360
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type JudicialSourceErrorCode = 'source_unavailable' | 'source_changed' | 'source_not_found'
|
|
2
|
+
|
|
3
|
+
export type RegulationSourceErrorCode =
|
|
4
|
+
| 'source_unavailable'
|
|
5
|
+
| 'source_changed'
|
|
6
|
+
| 'regulation_not_found'
|
|
7
|
+
| 'regulation_abolished'
|
|
8
|
+
| 'regulation_article_not_found'
|
|
9
|
+
|
|
10
|
+
const REGULATION_ERROR_MESSAGES: Record<RegulationSourceErrorCode, string> = {
|
|
11
|
+
source_unavailable: '法規查詢服務或法務部全國法規資料庫暫時無法使用',
|
|
12
|
+
source_changed: '法務部全國法規資料庫來源格式無法辨識',
|
|
13
|
+
regulation_not_found: '官方來源查無指定法規',
|
|
14
|
+
regulation_abolished: '指定法規已廢止,未回傳現行條文',
|
|
15
|
+
regulation_article_not_found: '官方來源查無指定條文',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class JudicialSourceError extends Error {
|
|
19
|
+
readonly code: JudicialSourceErrorCode
|
|
20
|
+
readonly sourceUrl: string
|
|
21
|
+
readonly retrievedAt: string
|
|
22
|
+
|
|
23
|
+
constructor(code: JudicialSourceErrorCode, sourceUrl: string) {
|
|
24
|
+
super(
|
|
25
|
+
code === 'source_changed'
|
|
26
|
+
? '司法院裁判書來源格式無法辨識'
|
|
27
|
+
: code === 'source_not_found'
|
|
28
|
+
? '司法院裁判書來源未找到指定裁判'
|
|
29
|
+
: '司法院公開裁判書來源暫時無法使用',
|
|
30
|
+
)
|
|
31
|
+
this.name = 'JudicialSourceError'
|
|
32
|
+
this.code = code
|
|
33
|
+
this.sourceUrl = sourceUrl
|
|
34
|
+
this.retrievedAt = new Date().toISOString()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class RegulationSourceError extends Error {
|
|
39
|
+
readonly code: RegulationSourceErrorCode
|
|
40
|
+
readonly sourceUrl: string
|
|
41
|
+
readonly retrievedAt: string
|
|
42
|
+
|
|
43
|
+
constructor(code: RegulationSourceErrorCode, sourceUrl: string) {
|
|
44
|
+
super(REGULATION_ERROR_MESSAGES[code])
|
|
45
|
+
this.name = 'RegulationSourceError'
|
|
46
|
+
this.code = code
|
|
47
|
+
this.sourceUrl = sourceUrl
|
|
48
|
+
this.retrievedAt = new Date().toISOString()
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/tools.ts
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
import { createBlobResponse } from './response.js'
|
|
3
|
+
import { fetchJudgmentText, searchJudgments } from './public-judgment-source.js'
|
|
4
|
+
import { JudicialSourceError, RegulationSourceError } from './source-error.js'
|
|
5
|
+
import { queryRegulation } from './regulation-source.js'
|
|
6
|
+
|
|
7
|
+
// ─── Arg interfaces ──────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export interface TokenArgs { token: string }
|
|
10
|
+
export interface GetJudgmentArgs { token: string; jid: string }
|
|
11
|
+
export interface ListResourcesArgs { categoryNo: string; token: string }
|
|
12
|
+
export interface DownloadFileArgs { fileSetId: string; token: string; top?: number; skip?: number }
|
|
13
|
+
export interface SearchJudgmentsArgs extends Record<string, unknown> {
|
|
14
|
+
keyword?: string
|
|
15
|
+
court?: string
|
|
16
|
+
year?: number
|
|
17
|
+
caseNumber?: string
|
|
18
|
+
limit?: number
|
|
19
|
+
page?: number
|
|
20
|
+
offset?: number
|
|
21
|
+
}
|
|
22
|
+
export interface FetchJudgmentTextArgs extends Record<string, unknown> { caseId: string }
|
|
23
|
+
export interface QueryRegulationArgs extends Record<string, unknown> {
|
|
24
|
+
lawName: string
|
|
25
|
+
articleNumber: string
|
|
26
|
+
refresh?: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ─── Input validation ────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const validateInput = {
|
|
32
|
+
required(params: Record<string, unknown>, required: string[]): void {
|
|
33
|
+
const missing = required.filter(key => !params[key])
|
|
34
|
+
if (missing.length > 0) {
|
|
35
|
+
throw new Error(`缺少必要參數: ${missing.join(', ')}`)
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
token(token: unknown): void {
|
|
40
|
+
if (!token || typeof token !== 'string' || token.trim().length === 0) {
|
|
41
|
+
throw new Error('無效的授權 Token')
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function optionalStringArgument(params: Record<string, unknown>, key: string): string | undefined {
|
|
47
|
+
const value = params[key]
|
|
48
|
+
if (value === undefined) return undefined
|
|
49
|
+
if (typeof value !== 'string') throw new Error(`參數 ${key} 必須是文字`)
|
|
50
|
+
return value
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function optionalNumberArgument(params: Record<string, unknown>, key: string): number | undefined {
|
|
54
|
+
const value = params[key]
|
|
55
|
+
if (value === undefined) return undefined
|
|
56
|
+
if (typeof value !== 'number') throw new Error(`參數 ${key} 必須是數字`)
|
|
57
|
+
return value
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function optionalBooleanArgument(params: Record<string, unknown>, key: string): boolean | undefined {
|
|
61
|
+
const value = params[key]
|
|
62
|
+
if (value === undefined) return undefined
|
|
63
|
+
if (typeof value !== 'boolean') throw new Error(`參數 ${key} 必須是布林值`)
|
|
64
|
+
return value
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function searchJudgmentsArgs(params: Record<string, unknown>): SearchJudgmentsArgs {
|
|
68
|
+
const args: SearchJudgmentsArgs = {}
|
|
69
|
+
const keyword = optionalStringArgument(params, 'keyword')
|
|
70
|
+
const court = optionalStringArgument(params, 'court')
|
|
71
|
+
const year = optionalNumberArgument(params, 'year')
|
|
72
|
+
const caseNumber = optionalStringArgument(params, 'caseNumber')
|
|
73
|
+
const limit = optionalNumberArgument(params, 'limit')
|
|
74
|
+
const page = optionalNumberArgument(params, 'page')
|
|
75
|
+
const offset = optionalNumberArgument(params, 'offset')
|
|
76
|
+
if (keyword !== undefined) args.keyword = keyword
|
|
77
|
+
if (court !== undefined) args.court = court
|
|
78
|
+
if (year !== undefined) args.year = year
|
|
79
|
+
if (caseNumber !== undefined) args.caseNumber = caseNumber
|
|
80
|
+
if (limit !== undefined) args.limit = limit
|
|
81
|
+
if (page !== undefined) args.page = page
|
|
82
|
+
if (offset !== undefined) args.offset = offset
|
|
83
|
+
return args
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function fetchJudgmentTextArgs(params: Record<string, unknown>): FetchJudgmentTextArgs {
|
|
87
|
+
validateInput.required(params, ['caseId'])
|
|
88
|
+
const caseId = params.caseId
|
|
89
|
+
if (typeof caseId !== 'string') throw new Error('參數 caseId 必須是文字')
|
|
90
|
+
return { caseId }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function queryRegulationArgs(params: Record<string, unknown>): QueryRegulationArgs {
|
|
94
|
+
validateInput.required(params, ['lawName', 'articleNumber'])
|
|
95
|
+
const lawName = optionalStringArgument(params, 'lawName')
|
|
96
|
+
const articleNumber = optionalStringArgument(params, 'articleNumber')
|
|
97
|
+
const refresh = optionalBooleanArgument(params, 'refresh')
|
|
98
|
+
if (!lawName || !articleNumber) throw new Error('法規名稱與條號不可為空')
|
|
99
|
+
return {
|
|
100
|
+
lawName,
|
|
101
|
+
articleNumber,
|
|
102
|
+
...(refresh === undefined ? {} : { refresh }),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── Error helper ────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
function apiErrorMessage(error: unknown): string {
|
|
109
|
+
if (error !== null && typeof error === 'object') {
|
|
110
|
+
const e = error as { response?: { data?: { message?: string } }; message?: string }
|
|
111
|
+
if (e.response?.data?.message) return e.response.data.message
|
|
112
|
+
if (e.message) return e.message
|
|
113
|
+
}
|
|
114
|
+
return String(error)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ─── API base URLs ───────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
const JUDICIAL_API_BASE = 'https://data.judicial.gov.tw/jdg/api'
|
|
120
|
+
const OPENDATA_API_BASE = 'https://opendata.judicial.gov.tw'
|
|
121
|
+
|
|
122
|
+
// ─── Tool definitions ────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
export const TOOLS_CONFIG = {
|
|
125
|
+
auth_token: {
|
|
126
|
+
name: 'auth_token',
|
|
127
|
+
description:
|
|
128
|
+
'取得司法院裁判書查詢系統的授權 Token。帳密從環境變數 JUDICIAL_USER / JUDICIAL_PASSWORD 讀取。',
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {},
|
|
132
|
+
additionalProperties: false,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
list_judgments: {
|
|
137
|
+
name: 'list_judgments',
|
|
138
|
+
description: '取得裁判書異動清單,包含新增、修改或刪除的裁判書資訊。',
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
token: { type: 'string', description: '從 auth_token 工具取得的授權 Token' },
|
|
143
|
+
},
|
|
144
|
+
required: ['token'],
|
|
145
|
+
additionalProperties: false,
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
get_judgment: {
|
|
150
|
+
name: 'get_judgment',
|
|
151
|
+
description: '取得特定裁判書的完整內容,包含裁判全文。',
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: 'object',
|
|
154
|
+
properties: {
|
|
155
|
+
token: { type: 'string', description: '從 auth_token 工具取得的授權 Token' },
|
|
156
|
+
jid: { type: 'string', description: '從 list_judgments 工具取得的裁判書 ID' },
|
|
157
|
+
},
|
|
158
|
+
required: ['token', 'jid'],
|
|
159
|
+
additionalProperties: false,
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
list_categories: {
|
|
164
|
+
name: 'list_categories',
|
|
165
|
+
description: '取得司法院開放資料平台的所有主題分類清單。',
|
|
166
|
+
inputSchema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
properties: {
|
|
169
|
+
token: { type: 'string', description: '從 member_token 工具取得的會員授權 Token' },
|
|
170
|
+
},
|
|
171
|
+
required: ['token'],
|
|
172
|
+
additionalProperties: false,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
list_resources: {
|
|
177
|
+
name: 'list_resources',
|
|
178
|
+
description: '取得指定分類下的所有資料源清單。',
|
|
179
|
+
inputSchema: {
|
|
180
|
+
type: 'object',
|
|
181
|
+
properties: {
|
|
182
|
+
categoryNo: {
|
|
183
|
+
type: 'string',
|
|
184
|
+
description: '從 list_categories 工具取得的分類編號',
|
|
185
|
+
},
|
|
186
|
+
token: { type: 'string', description: '從 member_token 工具取得的會員授權 Token' },
|
|
187
|
+
},
|
|
188
|
+
required: ['categoryNo', 'token'],
|
|
189
|
+
additionalProperties: false,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
download_file: {
|
|
194
|
+
name: 'download_file',
|
|
195
|
+
description: '下載指定檔案集的資料檔案,支援分頁參數。',
|
|
196
|
+
inputSchema: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
fileSetId: { type: 'string', description: '從 list_resources 工具取得的檔案集 ID' },
|
|
200
|
+
top: { type: 'integer', description: '限制回傳筆數(可選)', minimum: 0 },
|
|
201
|
+
skip: { type: 'integer', description: '跳過筆數,用於分頁(可選)', minimum: 0 },
|
|
202
|
+
token: { type: 'string', description: '從 member_token 工具取得的會員授權 Token' },
|
|
203
|
+
},
|
|
204
|
+
required: ['fileSetId', 'token'],
|
|
205
|
+
additionalProperties: false,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
member_token: {
|
|
210
|
+
name: 'member_token',
|
|
211
|
+
description:
|
|
212
|
+
'取得司法院開放平台的會員授權 Token,用於存取會員專屬資源。帳密從環境變數 JUDICIAL_USER / JUDICIAL_PASSWORD 讀取。',
|
|
213
|
+
inputSchema: {
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {},
|
|
216
|
+
additionalProperties: false,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
search_judgments: {
|
|
221
|
+
name: 'search_judgments',
|
|
222
|
+
description: '透過司法院公開裁判書查詢頁搜尋裁判,不需要授權 Token。year 可使用西元或民國年;完整案號可同時指定案類。',
|
|
223
|
+
inputSchema: {
|
|
224
|
+
type: 'object',
|
|
225
|
+
properties: {
|
|
226
|
+
keyword: { type: 'string', description: '裁判全文關鍵字,最多 128 字元' },
|
|
227
|
+
court: { type: 'string', description: '司法院法院名稱或官方法院代碼' },
|
|
228
|
+
year: { type: 'integer', description: '裁判年度,可使用西元或民國年', minimum: 1, maximum: 9999 },
|
|
229
|
+
caseNumber: { type: 'string', description: '案號數字,或「年度案類字第號」格式' },
|
|
230
|
+
limit: { type: 'integer', description: '每頁最多回傳筆數,預設 5,上限 10', minimum: 1, maximum: 10 },
|
|
231
|
+
page: { type: 'integer', description: '官方結果頁頁碼,預設 1,上限 50', minimum: 1, maximum: 50 },
|
|
232
|
+
offset: { type: 'integer', description: '官方結果頁內的起始位置,預設 0,範圍 0–19;依回傳的 next_page 與 next_offset 繼續查詢', minimum: 0, maximum: 19 },
|
|
233
|
+
},
|
|
234
|
+
additionalProperties: false,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
fetch_judgment_text: {
|
|
239
|
+
name: 'fetch_judgment_text',
|
|
240
|
+
description: '依 search_judgments 回傳的 case_id 取得司法院公開裁判全文,不需要授權 Token。',
|
|
241
|
+
inputSchema: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties: {
|
|
244
|
+
caseId: { type: 'string', description: 'search_judgments 回傳的 case_id' },
|
|
245
|
+
},
|
|
246
|
+
required: ['caseId'],
|
|
247
|
+
additionalProperties: false,
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
query_regulation: {
|
|
252
|
+
name: 'query_regulation',
|
|
253
|
+
description: '依官方法規名稱與條號查詢全國法規資料庫現行條文,回傳版本日期與官方來源;不提供歷史版本。',
|
|
254
|
+
inputSchema: {
|
|
255
|
+
type: 'object',
|
|
256
|
+
properties: {
|
|
257
|
+
lawName: { type: 'string', description: '法規名稱;若名稱不是唯一精確匹配,會回傳候選清單' },
|
|
258
|
+
articleNumber: { type: 'string', description: '條號數字,可含連字號,例如 1 或 1-1' },
|
|
259
|
+
refresh: { type: 'boolean', description: '設為 true 時略過快取並重新讀取官方來源' },
|
|
260
|
+
},
|
|
261
|
+
required: ['lawName', 'articleNumber'],
|
|
262
|
+
additionalProperties: false,
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
} as const
|
|
266
|
+
|
|
267
|
+
// ─── Tool handlers ───────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
export const TOOL_HANDLERS: Record<string, (_args: Record<string, unknown>) => Promise<unknown>> = {
|
|
270
|
+
async auth_token(_args) {
|
|
271
|
+
const user = process.env.JUDICIAL_USER
|
|
272
|
+
const password = process.env.JUDICIAL_PASSWORD
|
|
273
|
+
if (!user || !password) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
'未提供使用者帳號或密碼,且環境變數 JUDICIAL_USER 或 JUDICIAL_PASSWORD 未設定',
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const result = await axios.post(`${JUDICIAL_API_BASE}/Auth`, { user, password })
|
|
280
|
+
return result.data
|
|
281
|
+
} catch (error: unknown) {
|
|
282
|
+
throw new Error(`授權失敗: ${apiErrorMessage(error)}`)
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
async list_judgments(args) {
|
|
287
|
+
const typed = args as unknown as TokenArgs
|
|
288
|
+
validateInput.required(args, ['token'])
|
|
289
|
+
validateInput.token(typed.token)
|
|
290
|
+
try {
|
|
291
|
+
const result = await axios.post(`${JUDICIAL_API_BASE}/JList`, { token: typed.token })
|
|
292
|
+
return result.data
|
|
293
|
+
} catch (error: unknown) {
|
|
294
|
+
throw new Error(`取得裁判書清單失敗: ${apiErrorMessage(error)}`)
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
async get_judgment(args) {
|
|
299
|
+
const typed = args as unknown as GetJudgmentArgs
|
|
300
|
+
validateInput.required(args, ['token', 'jid'])
|
|
301
|
+
validateInput.token(typed.token)
|
|
302
|
+
try {
|
|
303
|
+
const result = await axios.post(`${JUDICIAL_API_BASE}/JDoc`, {
|
|
304
|
+
token: typed.token,
|
|
305
|
+
j: typed.jid,
|
|
306
|
+
})
|
|
307
|
+
return result.data
|
|
308
|
+
} catch (error: unknown) {
|
|
309
|
+
throw new Error(`取得裁判書內容失敗: ${apiErrorMessage(error)}`)
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
async list_categories(args) {
|
|
314
|
+
const typed = args as unknown as TokenArgs
|
|
315
|
+
validateInput.required(args, ['token'])
|
|
316
|
+
validateInput.token(typed.token)
|
|
317
|
+
try {
|
|
318
|
+
const result = await axios.get(`${OPENDATA_API_BASE}/data/api/rest/categories`, {
|
|
319
|
+
headers: { Authorization: `Bearer ${typed.token}` },
|
|
320
|
+
})
|
|
321
|
+
return result.data
|
|
322
|
+
} catch (error: unknown) {
|
|
323
|
+
throw new Error(`取得主題分類清單失敗: ${apiErrorMessage(error)}`)
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
async list_resources(args) {
|
|
328
|
+
const typed = args as unknown as ListResourcesArgs
|
|
329
|
+
validateInput.required(args, ['categoryNo', 'token'])
|
|
330
|
+
validateInput.token(typed.token)
|
|
331
|
+
try {
|
|
332
|
+
const url = `${OPENDATA_API_BASE}/data/api/rest/categories/${typed.categoryNo}/resources`
|
|
333
|
+
const result = await axios.get(url, {
|
|
334
|
+
headers: { Authorization: `Bearer ${typed.token}` },
|
|
335
|
+
})
|
|
336
|
+
return result.data
|
|
337
|
+
} catch (error: unknown) {
|
|
338
|
+
throw new Error(`取得資料源清單失敗: ${apiErrorMessage(error)}`)
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
async download_file(args) {
|
|
343
|
+
const typed = args as unknown as DownloadFileArgs
|
|
344
|
+
validateInput.required(args, ['fileSetId', 'token'])
|
|
345
|
+
validateInput.token(typed.token)
|
|
346
|
+
try {
|
|
347
|
+
const params = new URLSearchParams()
|
|
348
|
+
if (typed.top != null) params.append('top', String(typed.top))
|
|
349
|
+
if (typed.skip != null) params.append('skip', String(typed.skip))
|
|
350
|
+
let url = `${OPENDATA_API_BASE}/api/FilesetLists/${typed.fileSetId}/file`
|
|
351
|
+
if (params.toString()) url += `?${params.toString()}`
|
|
352
|
+
const result = await axios.get(url, {
|
|
353
|
+
responseType: 'arraybuffer',
|
|
354
|
+
headers: { Authorization: `Bearer ${typed.token}` },
|
|
355
|
+
})
|
|
356
|
+
const mimeType = ((result.headers['content-type'] as string) ?? '').split(';')[0].trim() || 'application/octet-stream'
|
|
357
|
+
const uri = `data:${mimeType};base64`
|
|
358
|
+
return createBlobResponse(Buffer.from(result.data as ArrayBuffer), mimeType, uri)
|
|
359
|
+
} catch (error: unknown) {
|
|
360
|
+
throw new Error(`檔案下載失敗: ${apiErrorMessage(error)}`)
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
async member_token(_args) {
|
|
365
|
+
const user = process.env.JUDICIAL_USER
|
|
366
|
+
const password = process.env.JUDICIAL_PASSWORD
|
|
367
|
+
if (!user || !password) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
'未提供使用者帳號或密碼,且環境變數 JUDICIAL_USER 或 JUDICIAL_PASSWORD 未設定',
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const result = await axios.post(`${OPENDATA_API_BASE}/api/MemberTokens`, {
|
|
374
|
+
memberAccount: user,
|
|
375
|
+
pwd: password,
|
|
376
|
+
})
|
|
377
|
+
return result.data
|
|
378
|
+
} catch (error: unknown) {
|
|
379
|
+
throw new Error(`會員授權失敗: ${apiErrorMessage(error)}`)
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
async search_judgments(args) {
|
|
384
|
+
try {
|
|
385
|
+
return await searchJudgments(searchJudgmentsArgs(args))
|
|
386
|
+
} catch (error: unknown) {
|
|
387
|
+
if (error instanceof JudicialSourceError) throw error
|
|
388
|
+
throw new Error(`搜尋裁判書失敗: ${apiErrorMessage(error)}`)
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
async fetch_judgment_text(args) {
|
|
393
|
+
try {
|
|
394
|
+
return await fetchJudgmentText(fetchJudgmentTextArgs(args))
|
|
395
|
+
} catch (error: unknown) {
|
|
396
|
+
if (error instanceof JudicialSourceError) throw error
|
|
397
|
+
throw new Error(`取得裁判全文失敗: ${apiErrorMessage(error)}`)
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
async query_regulation(args) {
|
|
402
|
+
try {
|
|
403
|
+
return await queryRegulation(queryRegulationArgs(args))
|
|
404
|
+
} catch (error: unknown) {
|
|
405
|
+
if (error instanceof RegulationSourceError) throw error
|
|
406
|
+
throw new Error(`查詢法規失敗: ${apiErrorMessage(error)}`)
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -1,113 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* @file types/index.d.ts
|
|
3
|
-
* @description TypeScript 類型定義檔案
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
declare module 'judicial-mcp' {
|
|
7
|
-
/**
|
|
8
|
-
* 司法院授權回應
|
|
9
|
-
*/
|
|
10
|
-
export interface AuthTokenResponse {
|
|
11
|
-
success: boolean;
|
|
12
|
-
message: string;
|
|
13
|
-
data: {
|
|
14
|
-
token: string;
|
|
15
|
-
expiry?: string;
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 裁判書清單項目
|
|
21
|
-
*/
|
|
22
|
-
export interface JudgmentListItem {
|
|
23
|
-
jid: string;
|
|
24
|
-
jname: string;
|
|
25
|
-
jdate: string;
|
|
26
|
-
action: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* 裁判書清單回應
|
|
31
|
-
*/
|
|
32
|
-
export interface JudgmentListResponse {
|
|
33
|
-
success: boolean;
|
|
34
|
-
message: string;
|
|
35
|
-
data: JudgmentListItem[];
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 裁判書內容回應
|
|
40
|
-
*/
|
|
41
|
-
export interface JudgmentContentResponse {
|
|
42
|
-
success: boolean;
|
|
43
|
-
message: string;
|
|
44
|
-
data: {
|
|
45
|
-
jid: string;
|
|
46
|
-
content: string;
|
|
47
|
-
metadata?: Record<string, any>;
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* 主題分類項目
|
|
53
|
-
*/
|
|
54
|
-
export interface CategoryItem {
|
|
55
|
-
categoryNo: string;
|
|
56
|
-
categoryName: string;
|
|
57
|
-
description?: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 資料源項目
|
|
62
|
-
*/
|
|
63
|
-
export interface ResourceItem {
|
|
64
|
-
fileSetId: string;
|
|
65
|
-
title: string;
|
|
66
|
-
description?: string;
|
|
67
|
-
format?: string;
|
|
68
|
-
updateTime?: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* 檔案下載回應
|
|
73
|
-
*/
|
|
74
|
-
export interface FileDownloadResponse {
|
|
75
|
-
success: boolean;
|
|
76
|
-
message: string;
|
|
77
|
-
data: {
|
|
78
|
-
size: number;
|
|
79
|
-
contentType: string;
|
|
80
|
-
base64: string;
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* MCP 工具參數類型
|
|
86
|
-
*/
|
|
87
|
-
export interface ToolArguments {
|
|
88
|
-
auth_token?: {
|
|
89
|
-
user?: string;
|
|
90
|
-
password?: string;
|
|
91
|
-
};
|
|
92
|
-
list_judgments: {
|
|
93
|
-
token: string;
|
|
94
|
-
};
|
|
95
|
-
get_judgment: {
|
|
96
|
-
token: string;
|
|
97
|
-
jid: string;
|
|
98
|
-
};
|
|
99
|
-
list_categories?: {};
|
|
100
|
-
list_resources: {
|
|
101
|
-
categoryNo: string;
|
|
102
|
-
};
|
|
103
|
-
download_file: {
|
|
104
|
-
fileSetId: string;
|
|
105
|
-
top?: string;
|
|
106
|
-
skip?: string;
|
|
107
|
-
};
|
|
108
|
-
member_token?: {
|
|
109
|
-
user?: string;
|
|
110
|
-
password?: string;
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
}
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface RegulationCacheEntry {
|
|
2
|
+
value: unknown;
|
|
3
|
+
expiresAt: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function getRegulationCacheEntry(key: string): RegulationCacheEntry | undefined;
|
|
6
|
+
export declare function setRegulationCacheEntry(key: string, entry: Omit<RegulationCacheEntry, 'expiresAt'>, now?: number): void;
|
|
7
|
+
export declare function deleteRegulationCacheEntry(key: string): void;
|
|
8
|
+
export declare function clearRegulationCache(): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
interface RegulationVersion {
|
|
2
|
+
amendment_date: string | null;
|
|
3
|
+
promulgation_date: string | null;
|
|
4
|
+
effective_date: string | null;
|
|
5
|
+
effective_status: 'as_stated' | 'pending_or_mixed' | 'unknown';
|
|
6
|
+
}
|
|
7
|
+
interface RegulationResult {
|
|
8
|
+
status: 'current' | 'ambiguous';
|
|
9
|
+
regulation_name?: string;
|
|
10
|
+
regulation_id?: string;
|
|
11
|
+
article_number?: string;
|
|
12
|
+
article_text?: string;
|
|
13
|
+
version?: RegulationVersion;
|
|
14
|
+
candidates?: Array<{
|
|
15
|
+
regulation_name: string;
|
|
16
|
+
regulation_id: string;
|
|
17
|
+
}>;
|
|
18
|
+
source_url: string;
|
|
19
|
+
search_source_url: string;
|
|
20
|
+
retrieved_at: string;
|
|
21
|
+
cache: {
|
|
22
|
+
status: 'miss' | 'hit' | 'refreshed' | 'revalidated' | 'not_cached';
|
|
23
|
+
};
|
|
24
|
+
truncated: boolean;
|
|
25
|
+
partial: boolean;
|
|
26
|
+
source_version: string | null;
|
|
27
|
+
}
|
|
28
|
+
export declare function queryRegulation(args: Record<string, unknown>): Promise<RegulationResult>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type ContentBlock = {
|
|
2
|
+
type: 'text';
|
|
3
|
+
text: string;
|
|
4
|
+
} | {
|
|
5
|
+
type: 'resource';
|
|
6
|
+
resource: {
|
|
7
|
+
uri: string;
|
|
8
|
+
mimeType: string;
|
|
9
|
+
blob: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export interface McpToolResult {
|
|
13
|
+
content: ContentBlock[];
|
|
14
|
+
isError?: boolean;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
export declare const createSuccessResponse: (data: unknown) => McpToolResult;
|
|
18
|
+
export declare const createErrorResponse: (error: unknown, message: string) => McpToolResult;
|
|
19
|
+
export declare const createBlobResponse: (data: Buffer | string, mimeType: string, uri: string) => McpToolResult;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type JudicialSourceErrorCode = 'source_unavailable' | 'source_changed' | 'source_not_found';
|
|
2
|
+
export type RegulationSourceErrorCode = 'source_unavailable' | 'source_changed' | 'regulation_not_found' | 'regulation_abolished' | 'regulation_article_not_found';
|
|
3
|
+
export declare class JudicialSourceError extends Error {
|
|
4
|
+
readonly code: JudicialSourceErrorCode;
|
|
5
|
+
readonly sourceUrl: string;
|
|
6
|
+
readonly retrievedAt: string;
|
|
7
|
+
constructor(code: JudicialSourceErrorCode, sourceUrl: string);
|
|
8
|
+
}
|
|
9
|
+
export declare class RegulationSourceError extends Error {
|
|
10
|
+
readonly code: RegulationSourceErrorCode;
|
|
11
|
+
readonly sourceUrl: string;
|
|
12
|
+
readonly retrievedAt: string;
|
|
13
|
+
constructor(code: RegulationSourceErrorCode, sourceUrl: string);
|
|
14
|
+
}
|