mcp_library_search 0.1.0__tar.gz
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.
- mcp_library_search-0.1.0/.gitignore +7 -0
- mcp_library_search-0.1.0/AGENTS.md +44 -0
- mcp_library_search-0.1.0/LICENSE +203 -0
- mcp_library_search-0.1.0/NOTICE +27 -0
- mcp_library_search-0.1.0/PKG-INFO +68 -0
- mcp_library_search-0.1.0/README.md +52 -0
- mcp_library_search-0.1.0/pyproject.toml +32 -0
- mcp_library_search-0.1.0/src/mcp_library_search/__init__.py +0 -0
- mcp_library_search-0.1.0/src/mcp_library_search/adapters/__init__.py +32 -0
- mcp_library_search-0.1.0/src/mcp_library_search/adapters/base.py +102 -0
- mcp_library_search-0.1.0/src/mcp_library_search/adapters/shanghai.py +74 -0
- mcp_library_search-0.1.0/src/mcp_library_search/server.py +77 -0
- mcp_library_search-0.1.0/src/mcp_library_search/vendor/__init__.py +0 -0
- mcp_library_search-0.1.0/src/mcp_library_search/vendor/shanghai_library/LICENSE +201 -0
- mcp_library_search-0.1.0/src/mcp_library_search/vendor/shanghai_library/__init__.py +0 -0
- mcp_library_search-0.1.0/src/mcp_library_search/vendor/shanghai_library/client.py +184 -0
- mcp_library_search-0.1.0/src/mcp_library_search/vendor/shanghai_library/library_client.py +298 -0
- mcp_library_search-0.1.0/src/mcp_library_search/vendor/shanghai_library/models.py +148 -0
- mcp_library_search-0.1.0/src/mcp_library_search/vendor/shanghai_library/parser.py +565 -0
- mcp_library_search-0.1.0/tests/test_adapter_contract.py +63 -0
- mcp_library_search-0.1.0/tests/test_server.py +55 -0
- mcp_library_search-0.1.0/tests/test_shanghai_adapter.py +155 -0
- mcp_library_search-0.1.0/tests/test_vendor_patches.py +104 -0
- mcp_library_search-0.1.0/uv.lock +2039 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
给在本仓库工作的 AI 编码助手和贡献者的指南。面向使用者的文档见 [README.md](README.md)。
|
|
4
|
+
|
|
5
|
+
## 架构约束(改代码前必读)
|
|
6
|
+
|
|
7
|
+
- `server.py` 是薄层:只做参数传递和异常包装。业务逻辑、对方代码的 import 一律不进这一层。
|
|
8
|
+
- 对方代码只允许在 `adapters/` 和 `vendor/` 中出现。适配层把对方接口收敛成干净函数,返回给 LLM 的字段在适配层裁剪,不透传原始 dataclass。
|
|
9
|
+
- 新增一座城市 = `adapters/<city>.py` 实现 `search_books` / `get_holdings` / `get_book_detail` 三个原语(返回结构对齐 `base.py` 的 TypedDict,由 `tests/test_adapter_contract.py` 强制校验),再在 `adapters/__init__.py` 的 `_ADAPTERS` 注册一行。server 和 tool 接口不动,README 的支持情况表加一行。
|
|
10
|
+
|
|
11
|
+
## vendor 目录(`src/mcp_library_search/vendor/`)
|
|
12
|
+
|
|
13
|
+
收录第三方项目代码(vendored),一个组件一个子目录,各目录保留其原始 LICENSE。
|
|
14
|
+
|
|
15
|
+
**NOTICE 约定**:根目录的 [NOTICE](NOTICE) 是所有第三方组件的唯一事实来源——每个组件一整块,记录主页、许可证、位置、变更。
|
|
16
|
+
|
|
17
|
+
- 新增组件:除放入 vendor/ 外,必须在 NOTICE 追加一块。
|
|
18
|
+
- 本地修改 vendored 代码:必须在 NOTICE 对应组件块的"变更"里补一条。
|
|
19
|
+
- 不要在 vendor 目录里放单独的 NOTICE 文件;README 致谢只点到为止,细节一律指向 NOTICE。
|
|
20
|
+
|
|
21
|
+
当前组件:shanghai-library-book-search-python([仓库](https://github.com/ZedeX/shanghai-library-book-search-python),Apache-2.0,位于 `vendor/shanghai_library/`)。
|
|
22
|
+
|
|
23
|
+
- 保持原样,bug 优先提给上游。
|
|
24
|
+
- 上游是平铺 import,已改为包内相对导入(仅 `library_client.py` 三行);**同步上游(覆盖同名文件)后必须重新应用相对导入修改**,否则包无法导入。同步后记得在 NOTICE 的变更里记一笔。
|
|
25
|
+
- 检索总条数:站点统计区已不再输出该信息(`total_results` 恒为 0),adapter 按"0 且当前页有结果 → 视为未知(null)"处理,不属于代码 bug。
|
|
26
|
+
- 图书简介:站点没有独立简介区块,内容简介以书目"附注"字段(MARC 500)形式给出,vendor 补丁将 summary 回退到附注(见 NOTICE 变更)。
|
|
27
|
+
- 预计归还时间:馆藏页不直接渲染归还日期,已借出馆藏的 `<a class="item-return-date">` 只带单册 `data-itemid`,需按条调用 `AJAX/JSON?method=itemReturnDate` 接口(vendor 提供 `get_return_date`,adapter 负责逐个补 `due_date`)。
|
|
28
|
+
- 本地补丁:`parser.py` 解析 has_next(下一页链接)、`library_client.py` 的 statistics 附带 has_next(见 NOTICE 变更)。同步上游覆盖文件后,检查这些补丁是否仍需重新应用。
|
|
29
|
+
|
|
30
|
+
## 测试
|
|
31
|
+
|
|
32
|
+
- `uv run pytest`。单测必须 mock 外部依赖,**不打真实图书馆网站**。
|
|
33
|
+
- 真网验证走手工 smoke,不进测试套件。
|
|
34
|
+
|
|
35
|
+
## 开发环境
|
|
36
|
+
|
|
37
|
+
- uv 管理。PyPI 不通时用镜像:`uv sync --default-index https://pypi.tuna.tsinghua.edu.cn/simple`。
|
|
38
|
+
- 调试 MCP:`uv run fastmcp dev src/mcp_library_search/server.py`(带 inspect 界面)。
|
|
39
|
+
|
|
40
|
+
## 发布
|
|
41
|
+
|
|
42
|
+
- 入口是 console script `mcp_library_search`(定义在 pyproject 的 `[project.scripts]`,转发到 `server:main`)。改动入口要同步检查 README 里的 uvx 用法。
|
|
43
|
+
- `uv build && uv publish`(需 PyPI token)。发布后 `uvx mcp_library_search` 生效。
|
|
44
|
+
- 改了 tool 的名称/参数/描述,同步更新 README 的 tool 表。
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Copyright 2026 mcp_library_search contributors
|
|
2
|
+
|
|
3
|
+
Apache License
|
|
4
|
+
Version 2.0, January 2004
|
|
5
|
+
http://www.apache.org/licenses/
|
|
6
|
+
|
|
7
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
8
|
+
|
|
9
|
+
1. Definitions.
|
|
10
|
+
|
|
11
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
12
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
13
|
+
|
|
14
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
15
|
+
the copyright owner that is granting the License.
|
|
16
|
+
|
|
17
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
18
|
+
other entities that control, are controlled by, or are under common
|
|
19
|
+
control with that entity. For the purposes of this definition,
|
|
20
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
21
|
+
direction or management of such entity, whether by contract or
|
|
22
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
23
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
24
|
+
|
|
25
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
26
|
+
exercising permissions granted by this License.
|
|
27
|
+
|
|
28
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
29
|
+
including but not limited to software source code, documentation
|
|
30
|
+
source, and configuration files.
|
|
31
|
+
|
|
32
|
+
"Object" form shall mean any form resulting from mechanical
|
|
33
|
+
transformation or translation of a Source form, including but
|
|
34
|
+
not limited to compiled object code, generated documentation,
|
|
35
|
+
and conversions to other media types.
|
|
36
|
+
|
|
37
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
38
|
+
Object form, made available under the License, as indicated by a
|
|
39
|
+
copyright notice that is included in or attached to the work
|
|
40
|
+
(an example is provided in the Appendix below).
|
|
41
|
+
|
|
42
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
43
|
+
form, that is based on (or derived from) the Work and for which the
|
|
44
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
45
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
46
|
+
of this License, Derivative Works shall not include works that remain
|
|
47
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
48
|
+
the Work and Derivative Works thereof.
|
|
49
|
+
|
|
50
|
+
"Contribution" shall mean any work of authorship, including
|
|
51
|
+
the original version of the Work and any modifications or additions
|
|
52
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
53
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
54
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
55
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
56
|
+
means any form of electronic, verbal, or written communication sent
|
|
57
|
+
to the Licensor or its representatives, including but not limited to
|
|
58
|
+
communication on electronic mailing lists, source code control systems,
|
|
59
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
60
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
61
|
+
excluding communication that is conspicuously marked or otherwise
|
|
62
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
63
|
+
|
|
64
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
65
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
66
|
+
subsequently incorporated within the Work.
|
|
67
|
+
|
|
68
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
69
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
70
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
71
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
72
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
73
|
+
Work and such Derivative Works in Source or Object form.
|
|
74
|
+
|
|
75
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
76
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
77
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
78
|
+
(except as stated in this section) patent license to make, have made,
|
|
79
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
80
|
+
where such license applies only to those patent claims licensable
|
|
81
|
+
by such Contributor that are necessarily infringed by their
|
|
82
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
83
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
84
|
+
institute patent litigation against any entity (including a
|
|
85
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
86
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
87
|
+
or contributory patent infringement, then any patent licenses
|
|
88
|
+
granted to You under this License for that Work shall terminate
|
|
89
|
+
as of the date such litigation is filed.
|
|
90
|
+
|
|
91
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
92
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
93
|
+
modifications, and in Source or Object form, provided that You
|
|
94
|
+
meet the following conditions:
|
|
95
|
+
|
|
96
|
+
(a) You must give any other recipients of the Work or
|
|
97
|
+
Derivative Works a copy of this License; and
|
|
98
|
+
|
|
99
|
+
(b) You must cause any modified files to carry prominent notices
|
|
100
|
+
stating that You changed the files; and
|
|
101
|
+
|
|
102
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
103
|
+
that You distribute, all copyright, patent, trademark, and
|
|
104
|
+
attribution notices from the Source form of the Work,
|
|
105
|
+
excluding those notices that do not pertain to any part of
|
|
106
|
+
the Derivative Works; and
|
|
107
|
+
|
|
108
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
109
|
+
distribution, then any Derivative Works that You distribute must
|
|
110
|
+
include a readable copy of the attribution notices contained
|
|
111
|
+
within such NOTICE file, excluding those notices that do not
|
|
112
|
+
pertain to any part of the Derivative Works, in at least one
|
|
113
|
+
of the following places: within a NOTICE text file distributed
|
|
114
|
+
as part of the Derivative Works; within the Source form or
|
|
115
|
+
documentation, if provided along with the Derivative Works; or,
|
|
116
|
+
within a display generated by the Derivative Works, if and
|
|
117
|
+
wherever such third-party notices normally appear. The contents
|
|
118
|
+
of the NOTICE file are for informational purposes only and
|
|
119
|
+
do not modify the License. You may add Your own attribution
|
|
120
|
+
notices within Derivative Works that You distribute, alongside
|
|
121
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
122
|
+
that such additional attribution notices cannot be construed
|
|
123
|
+
as modifying the License.
|
|
124
|
+
|
|
125
|
+
You may add Your own copyright statement to Your modifications and
|
|
126
|
+
may provide additional or different license terms and conditions
|
|
127
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
128
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
129
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
130
|
+
the conditions stated in this License.
|
|
131
|
+
|
|
132
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
133
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
134
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
135
|
+
this License, without any additional terms or conditions.
|
|
136
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
137
|
+
the terms of any separate license agreement you may have executed
|
|
138
|
+
with Licensor regarding such Contributions.
|
|
139
|
+
|
|
140
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
141
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
142
|
+
except as required for reasonable and customary use in describing the
|
|
143
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
144
|
+
|
|
145
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
146
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
147
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
148
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
149
|
+
implied, including, without limitation, any warranties or conditions
|
|
150
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
151
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
152
|
+
appropriateness of using or redistributing the Work and assume any
|
|
153
|
+
risks associated with Your exercise of permissions under this License.
|
|
154
|
+
|
|
155
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
156
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
157
|
+
unless required by applicable law (such as deliberate and grossly
|
|
158
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
159
|
+
liable to You for damages, including any direct, indirect, special,
|
|
160
|
+
incidental, or consequential damages of any character arising as a
|
|
161
|
+
result of this License or out of the use or inability to use the
|
|
162
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
163
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
164
|
+
other commercial damages or losses), even if such Contributor
|
|
165
|
+
has been advised of the possibility of such damages.
|
|
166
|
+
|
|
167
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
168
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
169
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
170
|
+
or other liability obligations and/or rights consistent with this
|
|
171
|
+
License. However, in accepting such obligations, You may act only
|
|
172
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
173
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
174
|
+
defend, and hold each Contributor harmless for any liability
|
|
175
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
176
|
+
of your accepting any such warranty or additional liability.
|
|
177
|
+
|
|
178
|
+
END OF TERMS AND CONDITIONS
|
|
179
|
+
|
|
180
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
181
|
+
|
|
182
|
+
To apply the Apache License to your work, attach the following
|
|
183
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
184
|
+
replaced with your own identifying information. (Don't include
|
|
185
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
186
|
+
comment syntax for the file format. We also recommend that a
|
|
187
|
+
file or class name and description of purpose be included on the
|
|
188
|
+
same "printed page" as the copyright notice for easier
|
|
189
|
+
identification within third-party archives.
|
|
190
|
+
|
|
191
|
+
Copyright [yyyy] [name of copyright owner]
|
|
192
|
+
|
|
193
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
194
|
+
you may not use this file except in compliance with the License.
|
|
195
|
+
You may obtain a copy of the License at
|
|
196
|
+
|
|
197
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
198
|
+
|
|
199
|
+
Unless required by applicable law or agreed to in writing, software
|
|
200
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
201
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
202
|
+
See the License for the specific language governing permissions and
|
|
203
|
+
limitations under the License.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
mcp_library_search
|
|
2
|
+
Copyright 2026 mcp_library_search contributors
|
|
3
|
+
|
|
4
|
+
本产品包含以下第三方项目的软件,以 vendored 方式收录于 src/mcp_library_search/vendor/。
|
|
5
|
+
各组件仍沿用其原始许可证,许可证文本随代码一并包含在对应目录中。
|
|
6
|
+
各组件的本地变更记录也一并列于下方。
|
|
7
|
+
|
|
8
|
+
组件:shanghai-library-book-search-python
|
|
9
|
+
主页:https://github.com/ZedeX/shanghai-library-book-search-python
|
|
10
|
+
许可证:Apache License 2.0
|
|
11
|
+
位置:src/mcp_library_search/vendor/shanghai_library/
|
|
12
|
+
变更:
|
|
13
|
+
- 保留核心检索模块(client.py、library_client.py、parser.py、models.py),
|
|
14
|
+
未引入其 Flask Web 层与 CLI 层。
|
|
15
|
+
- library_client.py 的 3 行平铺 import 改为包内相对导入,使其可作为包导入。
|
|
16
|
+
- parser.py:新增 has_next("下一页"链接)解析,随解析结果返回。
|
|
17
|
+
- library_client.py:statistics 增加 has_next 字段。
|
|
18
|
+
- parser.py:书目表新增"附注"字段提取(book_info["notes"])。
|
|
19
|
+
- library_client.py:summary 为空时回退取"附注"字段(站点无独立简介区块,
|
|
20
|
+
内容简介通常以 MARC 500 附注形式给出)。
|
|
21
|
+
- 说明:检索总条数(total_results)恒为 0 系站点不再输出该信息,由 adapter
|
|
22
|
+
按"总数为 0 且当前页有结果视为未知"规则处理,非代码缺陷。
|
|
23
|
+
- parser.py:馆藏行新增单册 item_id 提取(item-return-date 锚点的
|
|
24
|
+
data-itemid 属性,仅"已借出"馆藏有)。
|
|
25
|
+
- models.py:Availability 新增 item_id、due_date 字段。
|
|
26
|
+
- library_client.py:get_holdings 透传 item_id;新增 get_return_date
|
|
27
|
+
方法,按单册调用 AJAX itemReturnDate 接口查预计归还时间。
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mcp_library_search
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for finding books in city library systems (Shanghai supported; more cities coming)
|
|
5
|
+
Project-URL: Homepage, https://github.com/cnderrick/mcp_library_search
|
|
6
|
+
Project-URL: Repository, https://github.com/cnderrick/mcp_library_search
|
|
7
|
+
Project-URL: Issues, https://github.com/cnderrick/mcp_library_search/issues
|
|
8
|
+
Author-email: Derrick Huang <cnderrick.huang@gmail.com>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
License-File: NOTICE
|
|
12
|
+
Keywords: availability,books,library,mcp,shanghai
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: fastmcp>=2.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# mcp_library_search
|
|
18
|
+
|
|
19
|
+
MCP server:查询城市图书馆的馆藏与可借状态——回答"这本书在哪些馆能借到"。
|
|
20
|
+
|
|
21
|
+
## 支持情况
|
|
22
|
+
|
|
23
|
+
| 城市 | 标识 | 状态 | 数据源 |
|
|
24
|
+
|---|---|---|---|
|
|
25
|
+
| 上海 | `shanghai` | ✅ 已接入 | 上海中心图书馆"一卡通"总分馆体系(900+ 网点,含地铁站 24 小时自助机) |
|
|
26
|
+
| 深圳 | `shenzhen` | 🔜 待接入 | — |
|
|
27
|
+
| 更多城市 | — | 欢迎提需求或贡献适配器 | — |
|
|
28
|
+
|
|
29
|
+
## 安装
|
|
30
|
+
|
|
31
|
+
### Claude Code
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
claude mcp add mcp-library-search -- uvx mcp_library_search
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Claude Desktop
|
|
38
|
+
|
|
39
|
+
在 `claude_desktop_config.json` 里加:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"mcp-library-search": {
|
|
44
|
+
"command": "uvx",
|
|
45
|
+
"args": ["mcp_library_search"]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
改完重启桌面版。
|
|
51
|
+
|
|
52
|
+
## 使用
|
|
53
|
+
|
|
54
|
+
接入后提供 3 个 tool,查询链路为:先 `search_books` 按关键字找到 `book_id`,再用它查馆藏或详情。都带 `city` 参数(默认 `shanghai`,未接入的城市会返回明确提示):
|
|
55
|
+
|
|
56
|
+
| tool | 作用 |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `search_books(keyword, city, page, limit)` | 按关键字(书名、ISBN、作者等)搜书,返回分页列表,每条带 `book_id` 和可借概况 |
|
|
59
|
+
| `find_book_availability(book_id, city, only_available)` | 查这本书在哪些馆有、是否可借,可借的馆排前面;`only_available=False` 时已借出的馆藏带预计归还时间(`due_date`) |
|
|
60
|
+
| `get_book_detail(book_id, city)` | 查这本书的完整介绍(ISBN、索书号、内容简介) |
|
|
61
|
+
|
|
62
|
+
典型用法:对 Claude 说"帮我查《三体》在哪个馆能借到" → 搜书拿到 `book_id` → 查各馆可借状态 → 就近推荐。
|
|
63
|
+
|
|
64
|
+
## 致谢
|
|
65
|
+
|
|
66
|
+
- [shanghai-library-book-search-python](https://github.com/ZedeX/shanghai-library-book-search-python)——上海图书馆检索能力的来源(Apache-2.0)。
|
|
67
|
+
|
|
68
|
+
完整的第三方组件列表、归属与变更说明见 [NOTICE](NOTICE)。
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# mcp_library_search
|
|
2
|
+
|
|
3
|
+
MCP server:查询城市图书馆的馆藏与可借状态——回答"这本书在哪些馆能借到"。
|
|
4
|
+
|
|
5
|
+
## 支持情况
|
|
6
|
+
|
|
7
|
+
| 城市 | 标识 | 状态 | 数据源 |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| 上海 | `shanghai` | ✅ 已接入 | 上海中心图书馆"一卡通"总分馆体系(900+ 网点,含地铁站 24 小时自助机) |
|
|
10
|
+
| 深圳 | `shenzhen` | 🔜 待接入 | — |
|
|
11
|
+
| 更多城市 | — | 欢迎提需求或贡献适配器 | — |
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
### Claude Code
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
claude mcp add mcp-library-search -- uvx mcp_library_search
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Claude Desktop
|
|
22
|
+
|
|
23
|
+
在 `claude_desktop_config.json` 里加:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"mcp-library-search": {
|
|
28
|
+
"command": "uvx",
|
|
29
|
+
"args": ["mcp_library_search"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
改完重启桌面版。
|
|
35
|
+
|
|
36
|
+
## 使用
|
|
37
|
+
|
|
38
|
+
接入后提供 3 个 tool,查询链路为:先 `search_books` 按关键字找到 `book_id`,再用它查馆藏或详情。都带 `city` 参数(默认 `shanghai`,未接入的城市会返回明确提示):
|
|
39
|
+
|
|
40
|
+
| tool | 作用 |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `search_books(keyword, city, page, limit)` | 按关键字(书名、ISBN、作者等)搜书,返回分页列表,每条带 `book_id` 和可借概况 |
|
|
43
|
+
| `find_book_availability(book_id, city, only_available)` | 查这本书在哪些馆有、是否可借,可借的馆排前面;`only_available=False` 时已借出的馆藏带预计归还时间(`due_date`) |
|
|
44
|
+
| `get_book_detail(book_id, city)` | 查这本书的完整介绍(ISBN、索书号、内容简介) |
|
|
45
|
+
|
|
46
|
+
典型用法:对 Claude 说"帮我查《三体》在哪个馆能借到" → 搜书拿到 `book_id` → 查各馆可借状态 → 就近推荐。
|
|
47
|
+
|
|
48
|
+
## 致谢
|
|
49
|
+
|
|
50
|
+
- [shanghai-library-book-search-python](https://github.com/ZedeX/shanghai-library-book-search-python)——上海图书馆检索能力的来源(Apache-2.0)。
|
|
51
|
+
|
|
52
|
+
完整的第三方组件列表、归属与变更说明见 [NOTICE](NOTICE)。
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mcp_library_search"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server for finding books in city library systems (Shanghai supported; more cities coming)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
license-files = ["LICENSE", "NOTICE"]
|
|
9
|
+
authors = [{ name = "Derrick Huang", email = "cnderrick.huang@gmail.com" }]
|
|
10
|
+
keywords = ["mcp", "library", "shanghai", "books", "availability"]
|
|
11
|
+
dependencies = ["fastmcp>=2.0"]
|
|
12
|
+
|
|
13
|
+
[project.urls]
|
|
14
|
+
Homepage = "https://github.com/cnderrick/mcp_library_search"
|
|
15
|
+
Repository = "https://github.com/cnderrick/mcp_library_search"
|
|
16
|
+
Issues = "https://github.com/cnderrick/mcp_library_search/issues"
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
mcp_library_search = "mcp_library_search.server:main"
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = ["pytest>=8.0"]
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["hatchling"]
|
|
26
|
+
build-backend = "hatchling.build"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/mcp_library_search"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
testpaths = ["tests"]
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""适配层统一入口:按城市分派到具体适配器。
|
|
2
|
+
|
|
3
|
+
新增一座城市 = 在 adapters/ 下新建一个模块,实现 search_books / get_holdings /
|
|
4
|
+
get_book_detail 三个原语(返回结构对齐 base.py 的 TypedDict,由契约测试强制),
|
|
5
|
+
然后在本文件 _ADAPTERS 里注册一行。server 层与具体城市解耦。
|
|
6
|
+
"""
|
|
7
|
+
from . import shanghai
|
|
8
|
+
|
|
9
|
+
_ADAPTERS = {
|
|
10
|
+
"shanghai": shanghai,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_SUPPORTED = "、".join(sorted(_ADAPTERS))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get(city: str):
|
|
17
|
+
adapter = _ADAPTERS.get(city.lower())
|
|
18
|
+
if adapter is None:
|
|
19
|
+
raise RuntimeError(f"该城市暂未接入:{city}。当前支持:{_SUPPORTED}")
|
|
20
|
+
return adapter
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def search_books(city: str, keyword: str, page: int = 1, limit: int = 20) -> dict:
|
|
24
|
+
return _get(city).search_books(keyword, page=page, limit=limit)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_holdings(city: str, book_id: str, only_available: bool = True) -> list[dict]:
|
|
28
|
+
return _get(city).get_holdings(book_id, only_available=only_available)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_book_detail(city: str, book_id: str) -> dict:
|
|
32
|
+
return _get(city).get_book_detail(book_id)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""统一返回模型:所有城市适配器的输出必须对齐这里的 TypedDict。
|
|
2
|
+
|
|
3
|
+
字段语义以注释为准。tests/test_adapter_contract.py 会对 _ADAPTERS 里每个
|
|
4
|
+
适配器做契约校验——新增城市对齐不对齐,测试直接红。
|
|
5
|
+
"""
|
|
6
|
+
import types as _types
|
|
7
|
+
import typing
|
|
8
|
+
from typing import Optional, TypedDict, get_type_hints
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BookSummary(TypedDict):
|
|
12
|
+
"""search_books 的列表条目:轻量书目信息,不带简介和馆藏明细。"""
|
|
13
|
+
book_id: str # 城市内稳定的不透明 ID,原样传给 find_book_availability / get_book_detail
|
|
14
|
+
title: str
|
|
15
|
+
author: str # 没有则为空串
|
|
16
|
+
publisher: str # 没有则为空串
|
|
17
|
+
publish_year: str # 没有则为空串
|
|
18
|
+
availability_summary: str # 可借概况的简短文本,没有则为空串
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SearchPage(TypedDict):
|
|
22
|
+
total_results: Optional[int] # 匹配总数;None 表示数据源不提供总数
|
|
23
|
+
page: int # 当前页码(从 1 开始)
|
|
24
|
+
total_pages: int # 总页数
|
|
25
|
+
has_next: bool # 是否还有下一页
|
|
26
|
+
books: list[BookSummary]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Holding(TypedDict):
|
|
30
|
+
"""单条馆藏记录。"""
|
|
31
|
+
library: str # 分馆名称
|
|
32
|
+
location: str # 馆内位置(楼层/室),没有则为空串
|
|
33
|
+
call_number: str # 索书号,没有则为空串
|
|
34
|
+
status: str # 原始状态文本,如"可借"、"已借出"
|
|
35
|
+
available: bool # 可借与否的归一化结果
|
|
36
|
+
due_date: str # 预计归还时间(YYYY-MM-DD),仅已借出馆藏可能非空
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class BookDetail(TypedDict):
|
|
40
|
+
book_id: str
|
|
41
|
+
title: str
|
|
42
|
+
author: str
|
|
43
|
+
publisher: str
|
|
44
|
+
publish_year: str
|
|
45
|
+
isbn: str # 没有则为空串
|
|
46
|
+
call_number: str # 没有则为空串
|
|
47
|
+
summary: str # 内容简介,没有则为空串
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# ---------- 契约校验(测试用) ----------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _check(value, ann, where: str) -> None:
|
|
54
|
+
origin = typing.get_origin(ann)
|
|
55
|
+
if origin is typing.Union or origin is _types.UnionType: # Optional[X] / X | None
|
|
56
|
+
if value is None:
|
|
57
|
+
return
|
|
58
|
+
for arg in typing.get_args(ann):
|
|
59
|
+
if arg is not type(None):
|
|
60
|
+
_check(value, arg, where)
|
|
61
|
+
return
|
|
62
|
+
if origin is list:
|
|
63
|
+
if not isinstance(value, list):
|
|
64
|
+
raise AssertionError(f"{where}: 期望 list,实际 {type(value).__name__}")
|
|
65
|
+
(item_ann,) = typing.get_args(ann)
|
|
66
|
+
for i, item in enumerate(value):
|
|
67
|
+
_check(item, item_ann, f"{where}[{i}]")
|
|
68
|
+
return
|
|
69
|
+
if ann is int:
|
|
70
|
+
if not (isinstance(value, int) and not isinstance(value, bool)):
|
|
71
|
+
raise AssertionError(f"{where}: 期望 int,实际 {type(value).__name__}")
|
|
72
|
+
return
|
|
73
|
+
if typing.is_typeddict(ann): # 嵌套模型,如 list[BookSummary] 里的 BookSummary
|
|
74
|
+
if not isinstance(value, dict):
|
|
75
|
+
raise AssertionError(f"{where}: 期望 {ann.__name__}(dict),实际 {type(value).__name__}")
|
|
76
|
+
_check_model(value, ann, where)
|
|
77
|
+
return
|
|
78
|
+
if not isinstance(value, ann):
|
|
79
|
+
raise AssertionError(f"{where}: 期望 {ann.__name__},实际 {type(value).__name__}")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _check_model(obj: dict, model: type[TypedDict], where: str) -> None:
|
|
83
|
+
hints = get_type_hints(model)
|
|
84
|
+
expected, actual = set(hints), set(obj)
|
|
85
|
+
if expected != actual:
|
|
86
|
+
raise AssertionError(
|
|
87
|
+
f"{where}: 字段不匹配,缺 {sorted(expected - actual)},多 {sorted(actual - expected)}"
|
|
88
|
+
)
|
|
89
|
+
for key, ann in hints.items():
|
|
90
|
+
_check(obj[key], ann, f"{where}.{key}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def validate_search_page(page: SearchPage) -> None:
|
|
94
|
+
_check_model(page, SearchPage, "search_books")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def validate_holdings(holdings: "list[Holding]") -> None:
|
|
98
|
+
_check(holdings, list[Holding], "get_holdings")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def validate_book_detail(detail: BookDetail) -> None:
|
|
102
|
+
_check_model(detail, BookDetail, "get_book_detail")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""上海适配器:vendor 的 LibraryClient → base.py 统一模型。"""
|
|
2
|
+
from ..vendor.shanghai_library.library_client import LibraryClient
|
|
3
|
+
from .base import BookDetail, BookSummary, Holding, SearchPage
|
|
4
|
+
|
|
5
|
+
_client = LibraryClient()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def search_books(keyword: str, page: int = 1, limit: int = 20) -> SearchPage:
|
|
9
|
+
"""按关键词搜索馆藏。上游报错抛 RuntimeError。"""
|
|
10
|
+
result = _client.search(keyword=keyword, page=page, limit=limit)
|
|
11
|
+
if not result.success:
|
|
12
|
+
raise RuntimeError(f"上海图书馆搜索失败:{result.error}")
|
|
13
|
+
|
|
14
|
+
books: list[BookSummary] = [
|
|
15
|
+
{
|
|
16
|
+
"book_id": b.record_id,
|
|
17
|
+
"title": b.title,
|
|
18
|
+
"author": b.author,
|
|
19
|
+
"publisher": b.publisher,
|
|
20
|
+
"publish_year": b.publish_year,
|
|
21
|
+
"availability_summary": b.availability_summary,
|
|
22
|
+
}
|
|
23
|
+
for b in (result.books or [])
|
|
24
|
+
]
|
|
25
|
+
raw_total = result.statistics.get("total_results", 0)
|
|
26
|
+
return {
|
|
27
|
+
# 站点统计区不再输出总条数(解析结果恒为 0):
|
|
28
|
+
# 总数为 0 且当前页有结果 → 视为未知(null);当前页无结果 → 真的为 0
|
|
29
|
+
"total_results": raw_total if raw_total > 0 else (None if books else 0),
|
|
30
|
+
"page": result.statistics.get("page", page),
|
|
31
|
+
"total_pages": result.statistics.get("total_pages", 1),
|
|
32
|
+
"has_next": result.statistics.get("has_next", False),
|
|
33
|
+
"books": books,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_holdings(book_id: str, only_available: bool = True) -> list[Holding]:
|
|
38
|
+
"""指定图书在各分馆的馆藏与可借状态,可借的排前面。"""
|
|
39
|
+
holdings = _client.get_holdings(book_id) or []
|
|
40
|
+
kept = [h for h in holdings if (not only_available or h.is_available())]
|
|
41
|
+
items: list[Holding] = []
|
|
42
|
+
for h in kept:
|
|
43
|
+
available = h.is_available()
|
|
44
|
+
item: Holding = {
|
|
45
|
+
"library": h.library,
|
|
46
|
+
"location": h.location,
|
|
47
|
+
"call_number": h.call_number,
|
|
48
|
+
"status": h.status,
|
|
49
|
+
"available": available,
|
|
50
|
+
"due_date": "",
|
|
51
|
+
}
|
|
52
|
+
# 已借出的馆藏带单册 item_id,逐个查预计归还时间(单册级接口),失败留空串
|
|
53
|
+
if not available and getattr(h, "item_id", ""):
|
|
54
|
+
item["due_date"] = _client.get_return_date(h.item_id)
|
|
55
|
+
items.append(item)
|
|
56
|
+
items.sort(key=lambda h: (not h["available"], h["library"]))
|
|
57
|
+
return items
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def get_book_detail(book_id: str) -> BookDetail:
|
|
61
|
+
"""指定图书的完整书目信息(含 ISBN、内容简介)。查不到抛 RuntimeError。"""
|
|
62
|
+
book = _client.get_book_detail(book_id)
|
|
63
|
+
if book is None:
|
|
64
|
+
raise RuntimeError(f"未找到该书的详情:{book_id}")
|
|
65
|
+
return {
|
|
66
|
+
"book_id": book.record_id,
|
|
67
|
+
"title": book.title,
|
|
68
|
+
"author": book.author,
|
|
69
|
+
"publisher": book.publisher,
|
|
70
|
+
"publish_year": book.publish_year,
|
|
71
|
+
"isbn": book.isbn,
|
|
72
|
+
"call_number": book.call_number,
|
|
73
|
+
"summary": book.summary,
|
|
74
|
+
}
|