From 3ee23fe2c444444b90adb1e9a1a016e33f561f6f Mon Sep 17 00:00:00 2001 From: yuanzhipeng <2501363769@qq.com> Date: Sat, 20 Dec 2025 17:49:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(lzwcai-workflow-to-mcp):=20=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E5=8C=96=E9=A1=B9=E7=9B=AE=E5=B9=B6=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E5=8A=A8=E6=80=81=20MCP=20=E5=B7=A5=E5=85=B7=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 Python 3.13 版本声明文件 `.python-version` - 新增 `businessQueries.json`,包含三个 SQL 查询配置:按摘要、科目查凭证及科目余额 - 实现主程序 `main.py` 支持 local 和 api 两种模式加载配置,并注册为 MCP Server 工具 - 创建 `schema_converter.py` 用于将 sqlParams 转换为 MCP 输入 schema - 配置 `pyproject.toml` 定义项目元数据与依赖(mcp, anyio, requests) - 引入 utils 包结构,支持 API 请求、环境变量读取、日志配置等辅助功能模块 - 提供完整的工具列表和调用逻辑,通过 stdio 与 MCP 客户端通信执行工作流 --- .../lzwcai_workflow_to_mcp/.python-version | 1 + .../lzwcai_workflow_to_mcp/README.md | 0 .../__pycache__/main.cpython-312.pyc | Bin 0 -> 14034 bytes .../schema_converter.cpython-312.pyc | Bin 0 -> 6766 bytes .../businessQueries.json | 80 +++++ .../lzwcai_workflow_to_mcp/logs/.gitkeep | 0 .../lzwcai_workflow_to_mcp/main.py | 296 +++++++++++++++++ .../lzwcai_workflow_to_mcp/pyproject.toml | 18 + .../schema_converter.py | 208 ++++++++++++ .../lzwcai_workflow_to_mcp/utils/__init__.py | 33 ++ .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 839 bytes .../__pycache__/api_client.cpython-312.pyc | Bin 0 -> 11586 bytes .../__pycache__/env_config.cpython-312.pyc | Bin 0 -> 2917 bytes .../__pycache__/json_helper.cpython-312.pyc | Bin 0 -> 2311 bytes .../__pycache__/logger_config.cpython-312.pyc | Bin 0 -> 7496 bytes .../__pycache__/name_helper.cpython-312.pyc | Bin 0 -> 1543 bytes .../__pycache__/schema_helper.cpython-312.pyc | Bin 0 -> 3042 bytes .../utils/api_client.py | 307 ++++++++++++++++++ .../utils/env_config.py | 85 +++++ .../utils/json_helper.py | 59 ++++ .../utils/logger_config.py | 174 ++++++++++ .../utils/name_helper.py | 40 +++ .../utils/schema_helper.py | 94 ++++++ lzwcai_workflow_to_mcp/main.py | 16 + lzwcai_workflow_to_mcp/pyproject.toml | 35 ++ 25 files changed, 1446 insertions(+) create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/.python-version create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/README.md create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/__pycache__/main.cpython-312.pyc create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/__pycache__/schema_converter.cpython-312.pyc create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/businessQueries.json create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/logs/.gitkeep create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/main.py create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/pyproject.toml create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/schema_converter.py create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__init__.py create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/__init__.cpython-312.pyc create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/api_client.cpython-312.pyc create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/env_config.cpython-312.pyc create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/json_helper.cpython-312.pyc create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/logger_config.cpython-312.pyc create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/name_helper.cpython-312.pyc create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/schema_helper.cpython-312.pyc create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/api_client.py create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/env_config.py create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/json_helper.py create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/logger_config.py create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/name_helper.py create mode 100644 lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/schema_helper.py create mode 100644 lzwcai_workflow_to_mcp/main.py create mode 100644 lzwcai_workflow_to_mcp/pyproject.toml diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/.python-version b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/README.md b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/README.md new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/__pycache__/main.cpython-312.pyc b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1bb03141782ce502b12d9d82b87a247a55fb435d GIT binary patch literal 14034 zcmb_@dvp^=mUp$(ddsr>ke@sRHV-k_yh&mpGX?`g04Ea&EKWcX>b8-QC8t{g?2$7L zVJ3D0vAxN}o()ONE;%@ZCpf#=g_GBWnM{&B`^VRgm4l|oIWy<1W#Ic~6<|-YGyBKh zTdi)%ju^f@=c{8?Raf7tx>a3u@2_rM|65+3l|cBf(6#`#h>(BB2Q^sIncE-e2pJ@g z5sowwj^T7crir1cu1QBzeUqN1Y!gdULz97~#x_%viNQMhpt;S`WNEWDS?Mzyw6)or z>@;r(=CwJR91O7#&d8a9`EAZ7C%iFo=3qfvVN+pSQBx6pwgijYTum;Tw+2hvN}EdC z%9_d;q9c4MXXEv)YgOhm>8Sl8@Q;DgmGPAq8RfZY4%D-9dD`>b7UG-7&*$CcK)^5H z92+4G5Z^-H4Nv*_#5qrqrYarTPPl?ogez3rZmOnbMNn4ES6|epdsswET~Jz*Rl1lf z<(F_}e04L+m7ijpmU0!4*Kw7Q_waLDA5&^)#~0HNP-{MKYTcLpPRS$B(8iootV-Lo z9NK#PyOipCZZ2>!k9TvrP7{nwY59ro(nEiTzFUfQ;LVe{-%xIh-25zVR&j2ayVcwR zh}ZCy+`^+v2@%Zj4c4d}F{DG+p{pW%c{LH-baiCb12bzqFkEImFx-swaDenPr)|f{ z3HEuuF{%C~v9#2n| zT-d@#v?uT34sU>i*79eRLXl1`7WqiD-79v85x&hE47aodLM^ft3Gsm!vlany7% zP0QS^o;juE$l{LOpS>L$4J#@1`jit(hhHdj7ADq7V{?LuL&wD(q%tkuedD-MY5)SxzQ8saW zvo9Kq$m|h5*xqTYslg3b)9#BLX^bwxKEJbbZd|^U{OFW>DP_*B@Zs_3g)*QPDln>q zs57soMqM{G0WP`|%3!xneRy{IpP$*$;9d$WrYi$~$-YZByL&Z8VaHy7@lx{dpH21+ z-F&Xktv0lSbKiLJacNp z`1G6EyXbDkZN5OLw!K5n*EpfcRG#ZvUiElhwX|wmJa2pVwghXv z#+Ht=r3tfhGS8VPC>vTNIo%1TE0Om|%0>)jcZktoOgV_brV_f4&{>?wTc5H}0s|1( zvj|F6f~FY+_QGqnIpelD6SjF0JMT6yL}u)0+|3wnFNA22@C5J&LF@szL&vkcA+7uL zpg)ZrEPN$vgD>LS74~twKzBv<8czOVB>BdB*WYcs_nr9}MryI(hww@?L)1+s z9SbD$0-+in8a*c2?4=^m>y@ouFKonUkcYh8>wPNf3!-p@CzIF9h5e9Wg8>nC4;S!9 z1pJC`5!w#zSy&7OG};Oo5rx7j@_TlEie(H7`?;aD1Fd%mWR)67oO`lY!kbx>ZQ}Gi z3m}6%g%Hoc8+lWD>Wwh<<_?|5EZcUW{!7n2?#b`&Wkr_2cW9I?xbZBVD`k{k(bd8o zMMUPfjv)tNhXc$^L&GW(!F^5xK+Yu$$>Oa*eUnK!<6U{sIAur+@#_viuMt+Q*;=UP zGAZ;|-KD=rf2Noo#`m4JInt$nTE7QIiRp1hgSzHMnGre-(MWU6{UDXhe8?Z>0EtDN z(6I(dl?~{Q+M&=r{o1*kKRt8(!?)|)(NZW-9u&Mfb>&R*S3}9OBk-;W-ccy4K#ZF& z|Ki4_OLcCKRc6EOe2A`LSnaT%sYsHIY23#KLd{`WALK)F!8Ta>AV0T-qaj6d$_5?< zsGN^2H}JqLuSisuZVZ`e1}O)!TxNI?XVVQqk#Z>ZI-S{@1Lf;pC>1Y5)J;-4tEFVJ zz}3$Uh6X~z{;}=xf{iJn&ztixGg(?W*fG#CT<}t?pPh7;4B7^4(!APHacu3yj*+8R z=6uY37P=aewmv4!*%x>2Pn1@EV}hFBVD(!V{pF6CI2YU^I_Se**te^9msGiWZ0+R- z-+544y;&;X61O)bid@g{n9MJ_mR~iVUp14qcI2rMzf`_DZeIgrXLkH0Wq_vsEY<_> zC(G*Bk-w`q)O++-=UX6u)nlk%ZMs^k$NXv6G&24o1sM2D3DcZW5xfk8pTrI9!TpG)M$7lQ&6U+N8B~;sSkgB{pAt&jDrb1 zuB5S>h60u4-6-2scn3$C8M+6T8Pz=@$P9wuI>Crwwa5rW6?20&K(s(Z)>hA;~REL zb-N_j?k~-acTpp#Kk3}{LyT$!#tlWYgIF5TR7x(*4O%xnvnWhKkY=cJ&8{wYFEeL1 zqMe>{Q>#6KBFpraOFii`>Zw&z9+?rw7(6468dcRelZzPBnoF&i)ZRJs(fi&RiILp1-0d;A;3yn?f3pmSCoH);SsbN=Zfa?D1bCVa-31#<~AApb9^Aln?$ILm3 zpr)ZAri&D-c~zYYY)sd}bm>FuXV8^=hfW{U0aaBN$B0#D1uj2=7?p->urX8Rf||~Z zJJ$+y=3Z0q6$&DAwf6PuJkFf?7$|Z8BJ)%_00xV@*sLCNt$@N9i?hZRjTWmjqXG{A z1?hu|nGh~x*|Adk>w zrdY<%WTiEuh}PMXDvC#RO8wHAWlu{eggN z41_p1Udb#1f}5WMNpvAZ$}xOaf64Rj+<5oRIyaJT>NGBQw}~yAWIe>fLs)TU_ijWJ z?&Plqr{DhPI=9R=hC_T5wO)q9Y!WyoCSQLc`N7HLl`DXOa0>qF;K4+geEyB^OaY>= z4u&k zA~A(mYu;_R{LniOT{$7`dtBo9_}XS^O^Z~1ByJC+jKo>i7we4;ZJWri8Wz9EU#9jt zzy9N;pDn+-{PSZIoA*ie`=$K{r9;iv4z-RSYL$+*PaJwmIw(kU#kez)C* zabV-HaiVBRPlJZ-){Hl|ru&abkM5VA^nJPZu+)A`T60_~KM}Wg0OQVzq3ZK&JbzK5 zV%|{6fN`>*a;V{ab-Z9nqI~XvaSGuI?LU^nF{!L8ZhtyaRNk}u+o}ArnSiop#5b}>DqnS_p=VdzUOxj=E`v{V za^bcT^5*6>5dXG(^F~NMEwOAj=s%rbxt-`gtz#hn88JXi(W8K{Z#E`u#4?Ny!%sw{ zf|&5(uUVbpVP<&CZGvgaT$MqEkX4`p){}YxN_Vgy&2#lC7IgG9GjEKYRnLldDC3lM2~7@9;;Tf>=Axfr?r zcc)Z)F`!j?p1^w~x`1TsK3_1JKI`cO4$5VtG6ys4AbEa)_rZk(3K%#k9s#;O_=&qA z0y$Gm%=Vt+Cx0@TUopJzLesgXG26tVjq&_Vl6h0YlGpEge(q#`S^8BTSVQ9Z-Bkz-Q%`U%H^yCDvQncHy<-uMLh(8HiQj_==GsVHm4iD6cD}T`d-o)3 zeQxJ7JNuWN-E)5K#M~9qoR!kbA4tynFWJp>CjC%H3&`iQ@Vk8il>JV^z$|9j=5p=p zEMrpUMkF&!v(OpI&@_;P|4zUd1^ZpY`x+lGyPRG{OI^CV&}f(#&V7r(pp?5MO>zKL z;a#bWS^eqn(vpcYYdo=8^MVp_Rsh_uqsU$_7-D+P#@QGk$kn&R$NfCLd?JwjB2;uvv4eF)sTCpTEwO1TQ!WXmVjAM7t_ZKqebdz zN&UtQIh|zhwMvRJCdU|2Yr5rhB`m&-TLo57-Qp z?Gmj8rlw#1mt@bFdlwkj_u@UH8!$0IGlX*=zLe}eGZVncEVbwo0Y@lXao0Q|gxlHy zghfMvr=ol#z0!mmSGXa93)fEF;|HU721%Yjb-n**+1s?vjZJOpbS!sgUCC`~yh+V7 zO;D#(Ut3!nMa`ttQ{bZ;WUQNB;_jh$xB#f%VuZ(l3a-Z5AW?X;Wsy)0G9yea^jx4^cncHaDo_!ZBqR>SRsM^B|j`%_xn9cFtL%9PMVn8wiKXEY#up8vtZUPX@ zU_-ei75?CpqE8M^)bEjguvc4UY|8wA94@mp{65leh`$&&1;i?!s zFmPb#$0H3Bu2ntODb-N2a;$jFI2IZ|z|aa&C^ZTM{;B z_pbk<(|WSu{)Re+eg)a0-dBnX*joDH0j(&{c7R^qKi&hcrkNofbCfTK@`$yO{eN1`*OD?88&SbM5rL37HXtogI*J%6{+_lP?t0HSk!@ypZ(?a zH{MiUfT5!pi*nEkd$8TT5Xn~cw6cSv7fp&eA6igdH^Abp6MllEU>gFbB{51z(R%@% zTvReeU8}Yw%BJ>invyE^8Q=+zF02ik6JBm#lAD$2NTa znDqD)@udf)C5NN}Z=8J+`=2+sVqnGa@`=I~UlcaH`~8VE4Zx6chLH*v4IdpoC^^@} z*|jM%DP5Q-to+**L&DbveX+57*BxtS?C-Ldt?yW`Y)l)VRyIp3S|sO@I2%Z@HiLDl zU;*?|u%gF^%-ecxQc2Co(UF5v@rJm0V`f03kxQL#bzWKgc~m-hD89lg)jlb?d~x&P zJH^D}xMnUNHCNmTJkLT4F4Du%qzM<{m%>r(v^LyquFWHdT=rTIDv8xQaz?wRvEZI$ef*X zo%15Nk7!W)4-l~M!08hMc$(*^5oc6=7 z6n++TFQ7CmjE}Z)A2G~yIA9cCR4S6qQ8#pcf^XTV5XpKW8WQmP$WFXm%_bFa8o|rJ1io%O9B{JTpR7yo}62C zZSLCfxoamq8z(#uCTdrv3}oIqxP+cpc*kVQbKExB@~z(x8|)8^zR^RYwE3^G`EOG? zDEu~srQg!x%)9><_XATW^EW%00j7SP@o)8z`+2>!egoXSGxhfwuew>xSKFZc>I$a* z0ru)jBj)d8u>1iA^Batt^Gv^Cn9Z#HH#!sMEhZ@UL(^KoIm8LOeG7KSy)7Ua$QIF> z$O-W@8z=yUzfPHTtC|5!>JEYZh)a!z%xT(pFMyflz%mok-h&qw>}Y5gniXcXp+ThF z)KH3WV^)At=d2=l95txB9d39vAf0hby65a^7Gj{(Ky5CYMg-MA4ViD)VFJrBD)y-> z?yev#p1@&b17KYWZ-CTPHW+QbDS+#Rkw3_oUKu9k@y16*7iz(72OS! zj>7)6FSK@VS7{5Y&Tk)nKyuc^*;>s|&|f=Te4+eY`LIn=+RCd);qG*<+&Zt>^R-}C0njolEHxw z$KmyQ)+&BC6#b#nl}%~ycma0Y~3jYBLci+_U1dB7muo6+p2Nxgb+oi}wjZ zF)f(0%Bc!bB)C)Y>qj4Xbo~eOIa+IEXEYKBinVb2;PnRspe+=@iJMV<$A^xkJ?p8H ziCm;qQ5?F|H+HBX;0Bd#;13%XBGhq9HZ*MA{MdHI^=J!{oWY2ijaFd}En0HE*Xs*` z7Nh^VAc8X_T|Bs$6z|n_m}|s{LSVX}j$%b?mI3HEzDFyNY{`JvXYtv-qYYf8({NdK zZf!fva~#h-%8TKs02iZ(aOo}@Au z?0+DZKM=X0oie2?bcI($b?vm!M|9Ht~ zHCJoCB#->wQq=#$3CrAW|Pl3>Odc7h2D?jxShsi@@`3J<+-7o7DZ3xL}+q zm|%)hy48#^Va-eFA^lT+Q9sxFp)dbEYyRoBl%B8^-=-cn!!h6% tFpYD$1!nOpcI6-0hA-HLlm!d_ETXXcr#TJl$tUXCWitKF&G!b?$e*d;gk~V?^*h|DQTiGqK% zPt~q&Qd5YfIhxgQ8cxg6oUTP(0%|QZt35$C=~*4r8jk4@0tp&w^-ybM4MYQ%jZikR zCJt(_9!SYKXfs2bnag2w4zEUte}P@XTG(9H%I2~8t%$X~NwEb3x~3&6#Ob*umtl{eu)vD^F8L~-R%N1^~&({jT2LYH&PkC{hmA7hdqy`4Py(d-2j!l5d=H8mk#0$b0>*PG;)WUrr5Nnfc?( z$&+tGbq93U%DX$Bg_f&-n2CO}Hu=_TGZ#-wVpxy!ah{-)nH)Jab*gtZ(mQ+cI#l~Z z?T0uXlwKK5{=Q!}H;dPBuY(fR+x%Uk2HS2IwLBLL@qUlIyJVSZNb&yMIjF*Z8Aff{ ztwaPo2|V>OwBxUvT0}<8(rFphA~KqA#*TUs8EuvwdJ!3&vXkAKMI`jBAw3%HA~MES zl$O!4rm)&eb?d`0S8_&jSXEfhQV+tM!y5jfum(yxT=H)Q88eZ|%gC^1XwQ+QS7%Av zL3+tNi#onAtOE@Tv6u@oxkN_G<|@7VH5uz+nzbIG_^oNVO$HQ{&lQZ1R@S3UgZF6e zr<^C=6a?&$zKn+77&OeUg)!ujk!ixTk`KdrgtaZ|eP|^L5~Ra=L9?RWqAEwco`TUR z8c(6q_Z&r?>Zj0A%8?&Zg9{lM_~3vg&%81@^1Ec;i{Q4IQXqE4P4XmWyw_mbI)hBS_f+Li#{?hZmv z1UW^T1FRIa5@0%1dwJ}{qE7D0p%c}uTu@ZQxI~>-V7;wgD0%%sQPbiJfE^>*j~zT8 z;6=lJm;=c=3_J!=(MTN9Mdl{w$;{01yvX{9IdX5VBY1B`Bh1X@BhDb2vQ~<>V3U@+ z7E{0{m3S{se(2ocJlOSYwcC3TT!Fkg=1ZqhKU_@1p_XSTC7^yN0%qy2OB|-U(?YgGIGf!&pF_drIZ2lm=EAN2+u=2 ziJ(*k$}8$GIa?CDfAD85O1`Xr6m0+s!0qCfH z%v|w}sUZ6NkZIJkag^Tp{dXo*Q1+H#G{5p&MCEKrR6DUs98}a${6WZrjrdd1hB_Pi z+-9r0PyKl%1*vERk|XV)FozT17(zI~Gq894D41phVHA{E07^$rz6c^wI_k7(LHDjN zj4sO-x+y;}gKjFzB9)0GN-P$%EagSON>u|8Yclz6RhX(n&2_**)L9nPnFy4&K%KS) zvGnJgTSAK9r7&n$R#T4?)Jc#4{5XGh`FfOs~Hq6qINt z(IUy**qGT^XBLwMdI-!i8Mz5@GTC=+^2Xa!v1<}*g{J8%mnKKvpXzxv`O*6`@BDV^ zjVN@M?CGDmI7E6-a7T);5L1rN3!#i1BOVT_ot>Q!YSye-(@oOm%s}VG5!Q=I?V?ls zzhZje5Qf8rqNWT`UI^q)<~SMPoY~=7+LaiCBPWPRqgUoH#Y9mn_2E$Sm{bA!OLPSc z^Gl$Dufz$?ISiuFD|r1v(Czndd@+`$+k}8$)Id~W1<`_8PaWq8uu?z~X`fdJiZosr zf}^6=-O<7MS%;3l7i*gc>IyC*W+v_+<1dLsJdP3&^ho^WS3*I+LzKtQufnZ%1;8_& zg2TWZTQwl~IM)6Wk_h_FjzP3?z%!w<18y01Q(u=ZzfgI;a(C6c#6Jg|~IIO&e)UnNi-dZ;?TBe~;#COG#|i zTeb;HMZ#>GEU&wDAYQ%)u(yZqX^9^Gr^%97Thr4RTMEcG7#uTKiI&nt?$TJ(SnldC zbJvdLt{tYo$gN4`gRxX0G8d21Mc;q75#`#xw5%MntOQhq;U<=r4{yJK zBUJq$;4}W~g6&rHnYCnlwfeIv3Q|$?T!`bl=71vR3Ir4*1QZv!Qv6RBrOPrLc2dGB zpvwzixzdy#r1BP(f-8aodH|0LThg++w2V6I(vXJ5ZaPc>xnJzErB}wgh<(jZv>Se+ z-S`vjCZ(M<4`|q&0eT;DED7D81)9=jf;kj|W5x->FYxBc_j%>6zCRG~G14u;h))7Z z=7J{KbLU-FlGm=!ymv99@+0tdcNg-3Tj=hgz@+qou=q_Oi`ZgRmBgp|`er^|@M4h_ z85DFQBUeTuK^MUX!hQK%aB9&Qgtd8$hf5An+>qZ3>#!G7azpwJMv3MH5u* z=i1@26XbxSyMt~a5aK;tJuB+56-Ux-I}sSE7C6B zebk}ii?ApL9dLKXn zRCGBqTl+TmZjL??dpvGpB6SLxd?cP%IiAN3ao4=>dPmIhH9Mg6=YqeL{H0`cpKH|Z ziPy70HD%(uWr&Vjt0N7Q`8C6B@%(LoXn=7Ai6VQVXeDHU&tisJ_ww3AY1swGdBw+ z+B-08xTfGqL3Eh|N|<*LP&x>ie&9^_bp|+6Iq)1+7Dr0I*>$VKYL>#UgTQobVX6&l z-}Pnq5^$Uf;5ZZ2AnFTqd1t_|K~Lphenj)b-NTcp;M;ZRmR#KkR}dgz4764jo1Fah4y*xMMh zePLdn(t!*FO0zZ6$ZrP&qTc0dcYFOV7oU%dW+6nV3%M+Y@YBllJ7=d~zbZk*EDIIn zoq3;MT1P%J4j^c8h!}ru2P6;`e{e$^R9=SySkqHY?IwDex&))ep}o=A-iMgi6JxDF3!!* zo9g7v*JiF>OkV#m`SGQ>^V~GAw+-ty)Xbl0^D%RM<}%=P6pA{Riw$_do2p`XM*J#MAjZ8uhVRXb7b$%^QFziaF1^YPmOdO_ME(j2?Q_@9Ia-B_B5_4O4 zS%$Z4cpzaz?H#~miC~6O61aXB5l<&)>j(XV?EyCAc9As|*x< z$A&2L-%(*o`!Gdq8mdbn{6BnPWaBOR7CZ9LSk0b0q$;&jRZSHSZyd3`=S(5U-YUO? z|5DFTD=6(yc?!Yb@TNQXFSVPpP}ZUHq2RlfDFoRO+X#ET^bXF#%LHxiIX12?ktk~J z@>DHVtZr58PTmQLtmwwsX|DYl7Y_H9r|`7&Vqn& ''\n),\nmonth_filters AS (\n SELECT \n month::INT AS target_month\n FROM (\n SELECT unnest(string_to_array(nullif((SELECT months_param FROM query_params LIMIT 1), ''), ',')) AS month\n ) t\n WHERE month IS NOT NULL AND TRIM(month) <> ''\n)\n\nSELECT \n c.company_name AS 公司,\n v.dbill_date AS 日期,\n v.csign || LPAD(v.ino_id::text, 4, '0') AS 凭证号,\n v.cdigest AS 摘要,\n a.ccode AS 科目编码,\n a.ccode_name AS 科目名称,\n v.md AS 借方,\n v.mc AS 贷方,\n v.cbill AS 制单人\nFROM uf_voucher v\nJOIN uf_company c ON v.company_id = c.id\nJOIN uf_account_code a ON v.account_code_id = a.id\nCROSS JOIN query_params qp\nWHERE v.bdelete = false\n AND v.iyear = qp.target_year\n AND v.cdigest LIKE '%' || qp.digest_keyword || '%'\n AND (\n NOT EXISTS (SELECT 1 FROM company_filters)\n OR EXISTS (\n SELECT 1 FROM company_filters cf \n WHERE c.company_name LIKE '%' || cf.company_keyword || '%'\n )\n )\n AND (\n NOT EXISTS (SELECT 1 FROM month_filters)\n OR v.iperiod IN (SELECT target_month FROM month_filters)\n )\nORDER BY c.company_name, v.dbill_date DESC, v.ino_id;", + "sqlParams": "[{\"type\":\"paragraph\",\"name\":\"digest\",\"displayName\":\"摘要关键字\",\"maxLength\":500,\"defaultValue\":\"\",\"required\":true},{\"type\":\"string\",\"name\":\"company_names\",\"displayName\":\"公司名称\",\"maxLength\":200,\"defaultValue\":\"\",\"required\":false},{\"type\":\"number\",\"name\":\"year\",\"displayName\":\"会计年度\",\"maxLength\":4,\"defaultValue\":\"2025\",\"required\":true},{\"type\":\"string\",\"name\":\"months\",\"displayName\":\"月份\",\"maxLength\":50,\"defaultValue\":\"\",\"required\":false}]", + "resultType": "list", + "sourceType": "ai", + "trainingTaskId": null, + "tableMetadataIds": "", + "executionCount": 0, + "visualizationConfigs": [], + "inputJsonSchema": "{}", + "outputJsonSchema": "{\"type\":\"object\",\"properties\":{\"data\":{\"type\":\"array\"}}}", + "lastExecutionTime": null + }, + { + "id": "2000852304343240706", + "createBy": "bjt", + "createTime": "2025-12-16 16:55:38", + "updateBy": null, + "updateTime": null, + "serviceId": "2000821852159709186", + "uniqueName": "按科目查凭证", + "name": "ankemuchapingzheng_6a268ea1", + "description": "按照科目名称查询凭证信息", + "visualizable": 1, + "toolPrompt": "按照科目名称查询凭证信息", + "toolType": "sql", + "datasourceId": "158", + "sqlTemplate": "WITH query_params AS (\n SELECT \n {year}::INT AS target_year,\n '{company_names}' AS company_names_param,\n '{subject_name}' AS subject_keyword,\n '{months}' AS months_param\n),\ncompany_filters AS (\n SELECT \n TRIM(keyword) AS company_keyword\n FROM (\n SELECT unnest(string_to_array(nullif((SELECT company_names_param FROM query_params LIMIT 1), ''), ',')) AS keyword\n ) t\n WHERE keyword IS NOT NULL AND TRIM(keyword) <> ''\n),\nmonth_filters AS (\n SELECT \n month::INT AS target_month\n FROM (\n SELECT unnest(string_to_array(nullif((SELECT months_param FROM query_params LIMIT 1), ''), ',')) AS month\n ) t\n WHERE month IS NOT NULL AND TRIM(month) <> ''\n),\n目标科目 AS (\n SELECT DISTINCT a.id, a.ccode\n FROM uf_account_code a\n JOIN uf_company c ON a.company_id = c.id\n CROSS JOIN query_params qp\n WHERE a.ccode_name LIKE '%' || qp.subject_keyword || '%'\n AND a.iyear = qp.target_year\n AND (\n NOT EXISTS (SELECT 1 FROM company_filters)\n OR EXISTS (\n SELECT 1 FROM company_filters cf \n WHERE c.company_name LIKE '%' || cf.company_keyword || '%'\n )\n )\n),\n相关科目 AS (\n SELECT a.id\n FROM uf_account_code a\n JOIN uf_company c ON a.company_id = c.id\n CROSS JOIN query_params qp\n WHERE a.iyear = qp.target_year\n AND EXISTS (\n SELECT 1 FROM 目标科目 t \n WHERE a.ccode LIKE t.ccode || '%'\n )\n AND (\n NOT EXISTS (SELECT 1 FROM company_filters)\n OR EXISTS (\n SELECT 1 FROM company_filters cf \n WHERE c.company_name LIKE '%' || cf.company_keyword || '%'\n )\n )\n)\nSELECT \n c.company_name AS 公司,\n v.dbill_date AS 日期,\n v.csign || LPAD(v.ino_id::text, 4, '0') AS 凭证号,\n v.cdigest AS 摘要,\n a.ccode AS 科目编码,\n a.ccode_name AS 科目名称,\n a.igrade AS 级次,\n v.md AS 借方,\n v.mc AS 贷方\nFROM uf_voucher v\nJOIN uf_company c ON v.company_id = c.id\nJOIN uf_account_code a ON v.account_code_id = a.id\nCROSS JOIN query_params qp\nWHERE v.bdelete = false\n AND v.account_code_id IN (SELECT id FROM 相关科目)\n AND v.iyear = qp.target_year\n AND (\n NOT EXISTS (SELECT 1 FROM company_filters)\n OR EXISTS (\n SELECT 1 FROM company_filters cf \n WHERE c.company_name LIKE '%' || cf.company_keyword || '%'\n )\n )\n AND (\n NOT EXISTS (SELECT 1 FROM month_filters)\n OR v.iperiod IN (SELECT target_month FROM month_filters)\n )\nORDER BY c.company_name, a.ccode, v.dbill_date, v.ino_id;", + "sqlParams": "[{\"type\":\"select\",\"name\":\"subject_name\",\"displayName\":\"科目名称\",\"maxLength\":100,\"defaultValue\":\"\",\"required\":true,\"options\":[\"管理费用\",\"销售费用\",\"财务费用\",\"应收账款\",\"应付账款\"]},{\"type\":\"paragraph\",\"name\":\"company_names\",\"displayName\":\"公司名称列表\",\"maxLength\":300,\"defaultValue\":\"\",\"required\":false},{\"type\":\"number\",\"name\":\"year\",\"displayName\":\"年度\",\"maxLength\":4,\"defaultValue\":\"2025\",\"required\":true},{\"type\":\"select\",\"name\":\"months\",\"displayName\":\"期间月份\",\"maxLength\":30,\"defaultValue\":\"\",\"required\":false,\"options\":[\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\",\"9\",\"10\",\"11\",\"12\"]}]", + "resultType": "list", + "sourceType": "ai", + "trainingTaskId": null, + "tableMetadataIds": "", + "executionCount": 0, + "visualizationConfigs": [], + "inputJsonSchema": "{}", + "outputJsonSchema": "{\"type\":\"object\",\"properties\":{\"data\":{\"type\":\"array\"}}}", + "lastExecutionTime": null + }, + { + "id": "2000852610904920066", + "createBy": "bjt", + "createTime": "2025-12-16 16:56:51", + "updateBy": null, + "updateTime": null, + "serviceId": "2000821852159709186", + "uniqueName": "按科目查余额", + "name": "ankemuchayue_68b302c2", + "description": "按照科目名称查询余额", + "visualizable": 1, + "toolPrompt": "按照科目名称查询余额", + "toolType": "sql", + "datasourceId": "158", + "sqlTemplate": "WITH query_params AS (\n SELECT \n {year}::INT AS target_year,\n '{company_names}' AS company_names_param,\n '{subject_name}' AS subject_keyword,\n '{months}' AS months_param\n),\ncompany_filters AS (\n SELECT \n TRIM(keyword) AS company_keyword\n FROM (\n SELECT unnest(string_to_array(nullif((SELECT company_names_param FROM query_params LIMIT 1), ''), ',')) AS keyword\n ) t\n WHERE keyword IS NOT NULL AND TRIM(keyword) <> ''\n),\nmonth_filters AS (\n SELECT \n month::INT AS target_month\n FROM (\n SELECT unnest(string_to_array(nullif((SELECT months_param FROM query_params LIMIT 1), ''), ',')) AS month\n ) t\n WHERE month IS NOT NULL AND TRIM(month) <> ''\n),\n目标科目 AS (\n SELECT DISTINCT a.id, a.ccode\n FROM uf_account_code a\n JOIN uf_company c ON a.company_id = c.id\n CROSS JOIN query_params qp\n WHERE a.ccode_name LIKE '%' || qp.subject_keyword || '%'\n AND a.iyear = qp.target_year\n AND (\n NOT EXISTS (SELECT 1 FROM company_filters)\n OR EXISTS (\n SELECT 1 FROM company_filters cf \n WHERE c.company_name LIKE '%' || cf.company_keyword || '%'\n )\n )\n),\n相关科目 AS (\n SELECT a.id\n FROM uf_account_code a\n JOIN uf_company c ON a.company_id = c.id\n CROSS JOIN query_params qp\n WHERE a.iyear = qp.target_year\n AND EXISTS (\n SELECT 1 FROM 目标科目 t \n WHERE a.ccode LIKE t.ccode || '%'\n )\n AND (\n NOT EXISTS (SELECT 1 FROM company_filters)\n OR EXISTS (\n SELECT 1 FROM company_filters cf \n WHERE c.company_name LIKE '%' || cf.company_keyword || '%'\n )\n )\n)\nSELECT \n c.company_name AS 公司,\n a.ccode AS 科目编码,\n a.ccode_name AS 科目名称,\n a.igrade AS 级次,\n a.cclass AS 类别,\n b.iyear AS 年,\n b.iperiod AS 月,\n b.cbegind_c AS 期初方向,\n b.mb AS 期初余额,\n b.md AS 借方发生,\n b.mc AS 贷方发生,\n b.cendd_c AS 期末方向,\n b.me AS 期末余额\nFROM uf_account_balance b\nJOIN uf_company c ON b.company_id = c.id\nJOIN uf_account_code a ON b.account_code_id = a.id\nCROSS JOIN query_params qp\nWHERE b.iyear = qp.target_year\n AND b.account_code_id IN (SELECT id FROM 相关科目)\n AND (\n NOT EXISTS (SELECT 1 FROM company_filters)\n OR EXISTS (\n SELECT 1 FROM company_filters cf \n WHERE c.company_name LIKE '%' || cf.company_keyword || '%'\n )\n )\n AND (\n NOT EXISTS (SELECT 1 FROM month_filters)\n OR b.iperiod IN (SELECT target_month FROM month_filters)\n )\nORDER BY c.company_name, a.ccode, b.iperiod;", + "sqlParams": "[{\"type\":\"string\",\"name\":\"subject_name\",\"displayName\":\"科目名称关键字\",\"maxLength\":100,\"defaultValue\":\"银行存款\",\"required\":true},{\"type\":\"string\",\"name\":\"company_names\",\"displayName\":\"公司筛选\",\"maxLength\":150,\"defaultValue\":\"\",\"required\":false},{\"type\":\"number\",\"name\":\"year\",\"displayName\":\"查询年度\",\"maxLength\":4,\"defaultValue\":\"2025\",\"required\":true},{\"type\":\"number\",\"name\":\"months\",\"displayName\":\"查询月份\",\"maxLength\":2,\"defaultValue\":\"\",\"required\":false}]", + "resultType": "list", + "sourceType": "ai", + "trainingTaskId": null, + "tableMetadataIds": "", + "executionCount": 0, + "visualizationConfigs": [], + "inputJsonSchema": "{}", + "outputJsonSchema": "{\"type\":\"object\",\"properties\":{\"data\":{\"type\":\"array\"}}}", + "lastExecutionTime": null + } +] \ No newline at end of file diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/logs/.gitkeep b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/main.py b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/main.py new file mode 100644 index 0000000..dd4a0f6 --- /dev/null +++ b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/main.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +""" +Workflow to MCP Server +从 businessQueries.json 或 API 动态生成 MCP 工具 + +支持两种模式: +- local: 从本地 JSON 文件加载 +- api: 从远程 API 加载 +""" +import json +import os +import logging +import argparse +import anyio + +import mcp.types as types +from mcp.server import NotificationOptions, Server +from mcp.server.models import InitializationOptions +from mcp.server.stdio import stdio_server + +try: + from .schema_converter import convert_sql_params_to_input_schema + from .utils.api_client import execute_workflow, get_workflow_by_id + from .utils.env_config import get_workflow_id + from .utils.logger_config import setup_system_logging, get_logger +except ImportError: + from schema_converter import convert_sql_params_to_input_schema + from utils.api_client import execute_workflow, get_workflow_by_id + from utils.env_config import get_workflow_id + from utils.logger_config import setup_system_logging, get_logger + +# 初始化日志系统 +setup_system_logging(app_name="lzwcai_workflow_to_mcp", log_level=logging.DEBUG) +logger = get_logger(__name__) + +# 初始化 MCP Server +server = Server("workflow_mcp_server") + +# 全局配置 +_tools_config: list[dict] = [] +_config: dict = {} + + +def parse_arguments(): + """解析命令行参数""" + parser = argparse.ArgumentParser(description="Workflow MCP Server") + parser.add_argument( + "--mode", + type=str, + choices=["local", "api"], + default="api", + help="数据加载模式: local(本地JSON) 或 api(远程API,默认)" + ) + parser.add_argument( + "--json-path", + type=str, + default=None, + help="本地 JSON 文件路径 (local 模式)" + ) + parser.add_argument( + "--workflow-id", + type=str, + default=None, + help="工作流ID (api 模式,可选,默认从环境变量 workflowId 获取)" + ) + return parser.parse_args() + + +class DataLoader: + """数据加载器基类""" + + def load(self) -> list[dict]: + raise NotImplementedError + + +class LocalLoader(DataLoader): + """本地 JSON 文件加载器""" + + def __init__(self, json_path: str = None): + if json_path is None: + json_path = os.path.join(os.path.dirname(__file__), "businessQueries.json") + self.json_path = json_path + + def load(self) -> list[dict]: + try: + with open(self.json_path, "r", encoding="utf-8") as f: + data = json.load(f) + logger.info(f"从本地加载 {len(data)} 条配置: {self.json_path}") + return data + except FileNotFoundError: + logger.error(f"配置文件不存在: {self.json_path}") + return [] + except json.JSONDecodeError as e: + logger.error(f"JSON 解析错误: {e}") + return [] + + +class ApiLoader(DataLoader): + """API 远程加载器 - 使用 get_workflow_by_id 获取工作流配置""" + + def __init__(self, workflow_id: str = None): + self.workflow_id = workflow_id or get_workflow_id() + logger.debug(f"ApiLoader 初始化,工作流ID: {self.workflow_id}") + + def load(self) -> list[dict]: + try: + if not self.workflow_id: + logger.error("未提供工作流ID,请设置环境变量 workflowId") + return [] + + logger.info(f"开始从 API 加载工作流配置,工作流ID: {self.workflow_id}") + + # 调用 get_workflow_by_id 获取工作流配置 + response = get_workflow_by_id(self.workflow_id) + + logger.debug(f"API 响应原始数据: {json.dumps(response, ensure_ascii=False, indent=2)}") + + # 返回格式: {code, data, msg} + if response.get("code") != 200: + logger.error(f"获取工作流配置失败: code={response.get('code')}, msg={response.get('msg')}") + return [] + + data = response.get("data") + logger.debug(f"API 响应 data 字段: {json.dumps(data, ensure_ascii=False, indent=2) if data else 'None'}") + + # data 可能是单个对象或列表 + if isinstance(data, dict): + result = [data] + elif isinstance(data, list): + result = data + else: + logger.warning(f"API 响应 data 字段类型异常: {type(data)}") + result = [] + + logger.info(f"从 API 加载工作流配置成功,工作流ID: {self.workflow_id}, 配置数量: {len(result)}") + return result + + except Exception as e: + logger.error(f"API 请求失败: {e}", exc_info=True) + return [] + + +def create_loader(mode: str, **kwargs) -> DataLoader: + """ + 创建数据加载器 + + Args: + mode: "local" 或 "api" + kwargs: + - json_path: 本地 JSON 路径 (local 模式) + - workflow_id: 工作流ID (api 模式,可选,默认从环境变量获取) + """ + if mode == "local": + return LocalLoader(json_path=kwargs.get("json_path")) + elif mode == "api": + return ApiLoader(workflow_id=kwargs.get("workflow_id")) + else: + raise ValueError(f"不支持的模式: {mode}") + + +def init_tools(loader: DataLoader): + """初始化工具配置""" + global _tools_config + _tools_config = loader.load() + logger.info(f"已加载 {len(_tools_config)} 个工具配置") + + +@server.list_tools() +async def handle_list_tools() -> list[types.Tool]: + """列出所有可用工具""" + logger.info(f"收到 ListTools 请求,当前配置数量: {len(_tools_config)}") + tools = [] + + for query in _tools_config: + name = query.get("name", "") + description = query.get("description") or query.get("toolPrompt") or query.get("uniqueName", "") + sql_params = query.get("sqlParams", "[]") + + logger.debug(f"处理工具配置: name={name}, description={description[:50] if description else 'None'}...") + + # 转换参数为 inputSchema + input_schema = convert_sql_params_to_input_schema(sql_params) + logger.debug(f"工具 {name} 的 inputSchema: {json.dumps(input_schema, ensure_ascii=False)}") + + tools.append( + types.Tool( + name=name, + description=description, + inputSchema=input_schema + ) + ) + + logger.info(f"ListTools 响应: 返回 {len(tools)} 个工具") + return tools + + +@server.call_tool() +async def handle_call_tool( + name: str, + arguments: dict | None +) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: + """调用工具""" + logger.info(f"收到 CallTool 请求: name={name}, arguments={json.dumps(arguments, ensure_ascii=False) if arguments else 'None'}") + + # 查找对应的工具配置 + tool_config = None + for query in _tools_config: + if query.get("name") == name: + tool_config = query + break + + if tool_config is None: + logger.error(f"未找到工具配置: {name}") + raise ValueError(f"未知工具: {name}") + + logger.debug(f"找到工具配置: {json.dumps(tool_config, ensure_ascii=False, indent=2)}") + + # 获取工作流ID + workflow_id = tool_config.get("workflowId") or get_workflow_id() + logger.info(f"使用工作流ID: {workflow_id}") + + # 构建请求数据 + request_data = { + "workflowId": workflow_id, + "inputs": arguments or {} + } + + logger.info(f"执行工作流请求数据: {json.dumps(request_data, ensure_ascii=False, indent=2)}") + + try: + # 调用工作流执行API + result = execute_workflow(request_data) + logger.info(f"工作流执行成功: {workflow_id}") + logger.debug(f"工作流执行结果: {json.dumps(result, ensure_ascii=False, indent=2)}") + except Exception as e: + logger.error(f"工作流执行失败: {e}", exc_info=True) + result = { + "error": str(e), + "tool_name": name, + "workflow_id": workflow_id + } + + return [ + types.TextContent( + type="text", + text=json.dumps(result, ensure_ascii=False, indent=2) + ) + ] + + +async def run_server(): + """运行 MCP Server (stdio 模式)""" + async with stdio_server() as streams: + await server.run( + streams[0], + streams[1], + InitializationOptions( + server_name="workflow_mcp_server", + server_version="0.1.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + + +def main(): + """主入口""" + global _config + + logger.info("=" * 50) + logger.info("Workflow MCP Server 启动") + logger.info("=" * 50) + + # 解析命令行参数 + args = parse_arguments() + _config = vars(args) + logger.info(f"命令行参数: {_config}") + + # 创建加载器并初始化工具 + logger.info(f"使用模式: {args.mode}") + loader = create_loader( + mode=args.mode, + json_path=args.json_path, + workflow_id=args.workflow_id + ) + init_tools(loader) + + logger.info("开始运行 MCP Server (stdio 模式)") + # 运行服务器 + anyio.run(run_server) + + +if __name__ == "__main__": + main() diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/pyproject.toml b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/pyproject.toml new file mode 100644 index 0000000..1a485da --- /dev/null +++ b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "lzwcai-workflow-to-mcp" +version = "0.1.0" +description = "从 businessQueries 动态生成 MCP 工具" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "mcp>=1.0.0", + "anyio>=4.0.0", + "requests>=2.28.0", +] + +[project.scripts] +workflow-mcp = "main:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/schema_converter.py b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/schema_converter.py new file mode 100644 index 0000000..405489d --- /dev/null +++ b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/schema_converter.py @@ -0,0 +1,208 @@ +""" +Schema 转换器 +将 sqlParams 数组格式转换为 MCP 工具需要的 JSON Schema 格式 + +支持的类型: +- string: 文本输入 +- paragraph: 段落/多行文本 +- select: 下拉选项 +- number: 数字输入 +""" +import json +from typing import Any + + +def convert_param_to_schema_property(param: dict) -> tuple[str, dict, bool]: + """ + 将单个参数转换为 JSON Schema property + + Args: + param: 参数配置,格式如: + { + "type": "string", + "name": "company_names", + "displayName": "公司名称", + "maxLength": 200, + "defaultValue": "", + "required": true, + "options": ["选项1", "选项2"] # 仅 select 类型 + } + + Returns: + tuple: (property_name, property_schema, is_required) + """ + param_type = param.get("type", "string") + param_name = param.get("name", "") + display_name = param.get("displayName", param_name) + default_value = param.get("defaultValue", "") + max_length = param.get("maxLength") + is_required = param.get("required", False) + options = param.get("options", []) + + property_schema = { + "description": display_name + } + + if param_type == "string": + property_schema["type"] = "string" + if max_length: + property_schema["maxLength"] = max_length + + elif param_type == "paragraph": + property_schema["type"] = "string" + property_schema["format"] = "paragraph" + if max_length: + property_schema["maxLength"] = max_length + + elif param_type == "select": + property_schema["type"] = "string" + if options: + property_schema["enum"] = options + + elif param_type == "number": + property_schema["type"] = "number" + + else: + # 默认当作 string 处理 + property_schema["type"] = "string" + + # 添加默认值 + if default_value not in (None, ""): + if param_type == "number": + try: + property_schema["default"] = int(default_value) if str(default_value).isdigit() else float(default_value) + except (ValueError, TypeError): + property_schema["default"] = default_value + else: + property_schema["default"] = default_value + + return param_name, property_schema, is_required + + +def convert_sql_params_to_input_schema(sql_params: str | list) -> dict: + """ + 将 sqlParams 转换为 MCP 工具的 inputSchema + + Args: + sql_params: sqlParams 字段值,可以是 JSON 字符串或已解析的列表 + 格式: [{"type": "string", "name": "xxx", ...}, ...] + + Returns: + dict: MCP 工具的 inputSchema,格式如: + { + "type": "object", + "properties": {...}, + "required": [...] + } + """ + # 解析 JSON 字符串 + if isinstance(sql_params, str): + try: + params_list = json.loads(sql_params) + except json.JSONDecodeError: + return {"type": "object", "properties": {}, "required": []} + else: + params_list = sql_params + + if not isinstance(params_list, list): + return {"type": "object", "properties": {}, "required": []} + + input_schema = { + "type": "object", + "properties": {}, + "required": [] + } + + for param in params_list: + if not isinstance(param, dict): + continue + + name, schema, is_required = convert_param_to_schema_property(param) + + if name: + input_schema["properties"][name] = schema + if is_required: + input_schema["required"].append(name) + + return input_schema + + +def convert_business_query_to_tool(query: dict) -> dict: + """ + 将单个 businessQuery 转换为 MCP Tool 配置 + + Args: + query: businessQuery 对象 + + Returns: + dict: MCP Tool 配置 + { + "name": "工具名称", + "description": "工具描述", + "inputSchema": {...} + } + """ + name = query.get("name", "") + description = query.get("description") or query.get("toolPrompt") or query.get("uniqueName", "") + sql_params = query.get("sqlParams", "[]") + + input_schema = convert_sql_params_to_input_schema(sql_params) + + return { + "name": name, + "description": description, + "inputSchema": input_schema, + # 保留原始数据供后续使用 + "_raw": { + "id": query.get("id"), + "uniqueName": query.get("uniqueName"), + "sqlTemplate": query.get("sqlTemplate"), + "datasourceId": query.get("datasourceId"), + "toolType": query.get("toolType"), + } + } + + +def convert_all_queries_to_tools(queries: list[dict]) -> list[dict]: + """ + 将所有 businessQueries 转换为 MCP Tools 列表 + + Args: + queries: businessQueries 列表 + + Returns: + list: MCP Tools 配置列表 + """ + tools = [] + for query in queries: + tool = convert_business_query_to_tool(query) + if tool["name"]: + tools.append(tool) + return tools + + +# 测试用 +if __name__ == "__main__": + # 测试单个参数转换 + test_param = { + "type": "select", + "name": "subject_name", + "displayName": "科目名称", + "maxLength": 100, + "defaultValue": "", + "required": True, + "options": ["管理费用", "销售费用", "财务费用"] + } + + name, schema, required = convert_param_to_schema_property(test_param) + print(f"参数名: {name}") + print(f"Schema: {json.dumps(schema, ensure_ascii=False, indent=2)}") + print(f"必填: {required}") + print() + + # 测试完整 sqlParams 转换 + test_sql_params = '[{"type":"paragraph","name":"digest","displayName":"摘要关键字","maxLength":500,"defaultValue":"","required":true},{"type":"string","name":"company_names","displayName":"公司名称","maxLength":200,"defaultValue":"","required":false},{"type":"number","name":"year","displayName":"会计年度","maxLength":4,"defaultValue":"2025","required":true}]' + + input_schema = convert_sql_params_to_input_schema(test_sql_params) + print("InputSchema:") + print(json.dumps(input_schema, ensure_ascii=False, indent=2)) diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__init__.py b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__init__.py new file mode 100644 index 0000000..53da55d --- /dev/null +++ b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__init__.py @@ -0,0 +1,33 @@ +"""Utils package for lzwcai_workflow_to_mcp""" + +from .json_helper import load_json +from .name_helper import generate_tool_name +from .schema_helper import generate_input_schema, validate_input_schema +from .api_client import ( + WorkflowAPIClient, + get_workflow_by_id, + execute_workflow, + process_workflow_response, + get_default_client, + DEFAULT_TIMEOUT, + WORKFLOW_EXECUTE_TIMEOUT +) +from .env_config import get_workflow_id, get_backend_base_url, get_env_config, set_env_variable + +__all__ = [ + 'load_json', + 'generate_tool_name', + 'generate_input_schema', + 'validate_input_schema', + 'WorkflowAPIClient', + 'get_workflow_by_id', + 'execute_workflow', + 'process_workflow_response', + 'get_default_client', + 'DEFAULT_TIMEOUT', + 'WORKFLOW_EXECUTE_TIMEOUT', + 'get_workflow_id', + 'get_backend_base_url', + 'get_env_config', + 'set_env_variable' +] diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/__init__.cpython-312.pyc b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a94cf01e5481e0dd05f4cc3da6c7555456a56f7e GIT binary patch literal 839 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 literal 0 HcmV?d00001 diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/env_config.cpython-312.pyc b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/env_config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c048b2d017a88c05f297f0888d4628f07fa3f8b GIT binary patch literal 2917 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 GNa=rd)2`6~ literal 0 HcmV?d00001 diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/logger_config.cpython-312.pyc b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/logger_config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1da07dc8eb43a5ad2d4ecdf67a8f1dce5cd3b7d5 GIT binary patch literal 7496 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 J$4Jtd{V({qmb?G} literal 0 HcmV?d00001 diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/name_helper.cpython-312.pyc b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/name_helper.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b61c36d17d72783c9902647126bf5c81590cd1cf GIT binary patch literal 1543 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 httpx.Client: + """懒加载 HTTP 客户端""" + if self._client is None: + self._client = httpx.Client(timeout=self.default_timeout) + return self._client + + def _get_headers(self) -> Dict[str, str]: + """获取请求头""" + return { + 'X-API-Key': f'Bearer {self.token}', + } + + def get_workflow_by_id(self, workflow_id: str) -> Dict[str, Any]: + """ + 根据工作流ID获取工作流信息 + + Args: + workflow_id: 工作流ID + + Returns: + API响应数据 + + Raises: + Exception: 请求失败时抛出 + """ + url = f"{self.base_url}/system/workflowManage/getByWorkflowId/{workflow_id}" + + try: + logger.info(f"[API请求] GET {url}") + logger.debug(f"[API请求] Headers: {self._get_headers()}") + + response = self.client.get( + url, + headers=self._get_headers() + ) + + logger.info(f"[API响应] HTTP {response.status_code}") + logger.debug(f"[API响应] Headers: {dict(response.headers)}") + + response.raise_for_status() + data = response.json() + + logger.info(f"[API响应] 获取工作流配置成功: workflow_id={workflow_id}") + logger.debug(f"[API响应] Body: {json.dumps(data, ensure_ascii=False, indent=2)}") + + return data + + except httpx.TimeoutException: + error_msg = f"API请求超时: {url}" + logger.error(f"[API错误] {error_msg}") + raise Exception(error_msg) + + except httpx.HTTPStatusError as e: + error_msg = f"API请求失败 (HTTP {e.response.status_code}): {url}" + logger.error(f"[API错误] {error_msg}") + logger.error(f"[API错误] 响应内容: {e.response.text}") + raise Exception(error_msg) + + except httpx.RequestError as e: + error_msg = f"API请求异常: {url}, 错误: {str(e)}" + logger.error(f"[API错误] {error_msg}") + raise Exception(error_msg) + + except Exception as e: + error_msg = f"处理API响应时出错: {str(e)}" + logger.error(f"[API错误] {error_msg}", exc_info=True) + raise Exception(error_msg) + + def execute_workflow(self, request_data: Dict[str, Any]) -> Dict[str, Any]: + """ + 执行工作流 + + Args: + request_data: 请求数据,包含工作流执行所需的参数 + + Returns: + API响应数据 + + Raises: + Exception: 请求失败时抛出 + """ + url = f"{self.base_url}/open/workflow/execute" + + try: + headers = self._get_headers() + headers['Content-Type'] = 'application/json' + headers['Accept'] = '*/*' + + logger.info(f"[API请求] POST {url} (超时: {self.execute_timeout}s)") + logger.debug(f"[API请求] Headers: {headers}") + logger.debug(f"[API请求] Body: {json.dumps(request_data, ensure_ascii=False, indent=2)}") + + # 使用更长的超时时间执行工作流 + response = self.client.post( + url, + headers=headers, + json=request_data, + timeout=self.execute_timeout # 使用工作流执行专用超时时间 + ) + + logger.info(f"[API响应] HTTP {response.status_code}") + logger.debug(f"[API响应] Headers: {dict(response.headers)}") + + response.raise_for_status() + data = response.json() + + logger.info(f"[API响应] 执行工作流成功") + logger.debug(f"[API响应] Body: {json.dumps(data, ensure_ascii=False, indent=2)}") + + return data + + except httpx.TimeoutException: + error_msg = f"执行工作流API请求超时: {url}" + logger.error(f"[API错误] {error_msg}") + raise Exception(error_msg) + + except httpx.HTTPStatusError as e: + error_msg = f"执行工作流API请求失败 (HTTP {e.response.status_code}): {url}" + logger.error(f"[API错误] {error_msg}") + logger.error(f"[API错误] 响应内容: {e.response.text}") + raise Exception(error_msg) + + except httpx.RequestError as e: + error_msg = f"执行工作流API请求异常: {url}, 错误: {str(e)}" + logger.error(f"[API错误] {error_msg}") + raise Exception(error_msg) + + except Exception as e: + error_msg = f"处理执行工作流API响应时出错: {str(e)}" + logger.error(f"[API错误] {error_msg}", exc_info=True) + raise Exception(error_msg) + + def close(self): + """关闭HTTP客户端""" + if self._client is not None: + self._client.close() + self._client = None + + def __enter__(self): + """支持 context manager""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """退出时关闭客户端""" + self.close() + return False + + +# 懒加载的默认客户端 +_default_client: Optional[WorkflowAPIClient] = None + + +def get_default_client() -> WorkflowAPIClient: + """获取默认客户端(懒加载)""" + global _default_client + if _default_client is None: + _default_client = WorkflowAPIClient() + return _default_client + + +def get_workflow_by_id(workflow_id: str, base_url: Optional[str] = None, token: Optional[str] = None) -> Dict[str, Any]: + """ + 便捷函数:根据工作流ID获取工作流信息 + + Args: + workflow_id: 工作流ID + base_url: API基础URL(可选) + token: 认证令牌(可选) + + Returns: + API响应数据 + """ + if base_url or token: + with WorkflowAPIClient(base_url=base_url, token=token) as client: + return client.get_workflow_by_id(workflow_id) + else: + return get_default_client().get_workflow_by_id(workflow_id) + + +def execute_workflow( + request_data: Dict[str, Any], + base_url: Optional[str] = None, + token: Optional[str] = None, + timeout: float = WORKFLOW_EXECUTE_TIMEOUT +) -> Dict[str, Any]: + """ + 便捷函数:执行工作流 + + Args: + request_data: 请求数据 + base_url: API基础URL(可选) + token: 认证令牌(可选) + timeout: 超时时间(秒),默认300秒(5分钟) + + Returns: + API响应数据 + """ + if base_url or token: + with WorkflowAPIClient(base_url=base_url, token=token, execute_timeout=timeout) as client: + return client.execute_workflow(request_data) + else: + # 更新默认客户端的超时时间 + default_client = get_default_client() + default_client.execute_timeout = timeout + return default_client.execute_workflow(request_data) + + +def process_workflow_response(response: Dict[str, Any]) -> List[Dict[str, Any]]: + """ + 处理API响应数据,映射为工作流配置格式 + + Args: + response: API原始响应数据 + + Returns: + 处理后的工作流配置列表 + """ + try: + data_list = response.get("data", []) + + # 如果 data 是单个对象而不是列表,转换为列表 + if isinstance(data_list, dict): + data_list = [data_list] + + workflows = [] + for workflow in data_list: + # 解析 inputParams 字符串为 JSON 对象 + input_params = workflow.get("inputParams", {}) + if isinstance(input_params, str): + try: + input_params = json.loads(input_params) + except json.JSONDecodeError: + input_params = {} + + # 映射字段 + config = { + "id": workflow.get("id"), + "workflowId": workflow.get("workflowId"), + "workflowName": workflow.get("workflowName") or workflow.get("name"), + "workflowDescription": workflow.get("workflowDescription") or workflow.get("description"), + "inputParams": input_params + } + workflows.append(config) + + logger.info(f"成功处理 {len(workflows)} 条工作流数据") + return workflows + + except Exception as e: + logger.error(f"处理API响应数据失败: {e}", exc_info=True) + raise diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/env_config.py b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/env_config.py new file mode 100644 index 0000000..eac8f35 --- /dev/null +++ b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/env_config.py @@ -0,0 +1,85 @@ +"""环境变量配置模块""" + +import os +from typing import Optional + + +def get_workflow_id(default: str = "") -> str: + """ + 获取工作流ID环境变量 + + Args: + default: 默认值(默认为 "") + + Returns: + str: 工作流ID + + Environment Variables: + workflowId: 工作流ID + """ + return os.environ.get("workflowId", default) + + +def get_backend_base_url(default: str = "http://lzwcai-demp-corp-manager:8086") -> str: + """ + 获取后端API基础URL环境变量 + + Args: + default: 默认值(默认为 "http://lzwcai-demp-corp-manager:8086") + + Returns: + str: 后端API基础URL + + Environment Variables: + backendBaseUrl: 后端API基础URL + """ + return os.environ.get("backendBaseUrl", default) + + +def get_workflow_execute_key(default: str = "") -> str: + """ + 获取工作流执行密钥(Token)环境变量 + + Args: + default: 默认值(默认为 "") + + Returns: + str: 工作流执行密钥 + + Environment Variables: + workflowExecuteKey: 工作流执行密钥/Token + """ + return os.environ.get("workflowExecuteKey", default) + + +def get_env_config() -> dict: + """ + 获取所有环境配置 + + Returns: + dict: 包含所有配置的字典 + + Example: + config = get_env_config() + print(config['workflow_id']) # 输出: "" + print(config['backend_base_url']) # 输出: "http://lzwcai-demp-corp-manager:8086" + """ + return { + "workflow_id": get_workflow_id(), + "backend_base_url": get_backend_base_url(), + "workflow_execute_key": get_workflow_execute_key() + } + + +def set_env_variable(key: str, value: str) -> None: + """ + 设置环境变量(仅在当前进程中有效) + + Args: + key: 环境变量名 + value: 环境变量值 + + Example: + set_env_variable("workflowId", "1234567890") + """ + os.environ[key] = value diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/json_helper.py b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/json_helper.py new file mode 100644 index 0000000..cd6894e --- /dev/null +++ b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/json_helper.py @@ -0,0 +1,59 @@ +"""JSON 文件读取工具""" + +import json +from pathlib import Path +from typing import Any, Union + + +def load_json(json_path: Union[str, Path]) -> Any: + """ + 读取 JSON 文件并返回其内容 + + Args: + json_path: JSON 文件的路径(支持字符串或 Path 对象) + + Returns: + JSON 文件中解析后的数据(可以是字典、列表或其他 JSON 类型) + + Raises: + FileNotFoundError: 当文件不存在时 + json.JSONDecodeError: 当 JSON 格式无效时 + Exception: 其他读取错误 + + Example: + >>> data = load_json('config.json') + >>> print(data) + {'key': 'value'} + + >>> data = load_json(Path('data/users.json')) + >>> print(data) + [{'id': 1, 'name': 'Alice'}] + """ + try: + # 转换为 Path 对象 + path = Path(json_path) + + # 检查文件是否存在 + if not path.exists(): + raise FileNotFoundError(f"JSON 文件不存在: {json_path}") + + # 检查是否为文件 + if not path.is_file(): + raise ValueError(f"路径不是一个文件: {json_path}") + + # 读取并解析 JSON 文件 + with open(path, 'r', encoding='utf-8') as f: + data = json.load(f) + + return data + + except json.JSONDecodeError as e: + raise json.JSONDecodeError( + f"JSON 格式错误: {e.msg}", + e.doc, + e.pos + ) + except FileNotFoundError: + raise + except Exception as e: + raise Exception(f"读取 JSON 文件时发生错误: {str(e)}") diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/logger_config.py b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/logger_config.py new file mode 100644 index 0000000..38efa16 --- /dev/null +++ b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/logger_config.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +""" +统一日志配置模块 +提供系统级别的日志配置和管理 +""" + +import os +import sys +import logging +from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler +from datetime import datetime +from pathlib import Path + + +class LoggerConfig: + """日志配置管理类""" + + def __init__(self, logs_dir: str = None): + """初始化日志配置 + + Args: + logs_dir: 日志目录路径,默认为项目根目录下的logs文件夹 + """ + if logs_dir: + self.logs_dir = Path(logs_dir) + else: + project_root = Path(__file__).parent.parent + self.logs_dir = project_root / "logs" + + self.logs_dir.mkdir(exist_ok=True) + + self.log_format = '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s' + self.date_format = '%Y-%m-%d %H:%M:%S' + self.log_level = self._get_log_level_from_env() + self._initialized = False + + def _get_log_level_from_env(self) -> int: + log_level_str = os.getenv('LOG_LEVEL', 'INFO').upper() + level_mapping = { + 'DEBUG': logging.DEBUG, + 'INFO': logging.INFO, + 'WARNING': logging.WARNING, + 'WARN': logging.WARNING, + 'ERROR': logging.ERROR, + 'CRITICAL': logging.CRITICAL, + 'FATAL': logging.CRITICAL + } + return level_mapping.get(log_level_str, logging.INFO) + + def setup_logging(self, + app_name: str = "lzwcai_workflow_to_mcp", + log_level: int = logging.INFO, + max_file_size: int = 10 * 1024 * 1024, + backup_count: int = 5, + console_output: bool = True) -> logging.Logger: + if self._initialized: + return logging.getLogger() + + root_logger = logging.getLogger() + root_logger.setLevel(log_level) + + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + formatter = logging.Formatter(self.log_format, self.date_format) + + # 1. 主日志文件 - 按大小滚动 + main_log_file = self.logs_dir / f"{app_name}.log" + file_handler = RotatingFileHandler( + main_log_file, + maxBytes=max_file_size, + backupCount=backup_count, + encoding='utf-8' + ) + file_handler.setLevel(log_level) + file_handler.setFormatter(formatter) + root_logger.addHandler(file_handler) + + # 2. 错误日志文件 + error_log_file = self.logs_dir / f"{app_name}_error.log" + error_handler = RotatingFileHandler( + error_log_file, + maxBytes=max_file_size, + backupCount=backup_count, + encoding='utf-8' + ) + error_handler.setLevel(logging.ERROR) + error_handler.setFormatter(formatter) + root_logger.addHandler(error_handler) + + # 3. 按日期滚动的日志文件 + daily_log_file = self.logs_dir / f"{app_name}_daily.log" + daily_handler = TimedRotatingFileHandler( + daily_log_file, + when='midnight', + interval=1, + backupCount=30, + encoding='utf-8' + ) + daily_handler.setLevel(log_level) + daily_handler.setFormatter(formatter) + daily_handler.suffix = "%Y-%m-%d" + root_logger.addHandler(daily_handler) + + # 4. 控制台输出 (MCP协议使用stdio时,必须将日志输出到stderr) + if console_output: + console_handler = logging.StreamHandler(sys.stderr) + console_handler.setLevel(log_level) + console_formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + self.date_format + ) + console_handler.setFormatter(console_formatter) + root_logger.addHandler(console_handler) + + self._initialized = True + root_logger.info(f"日志系统初始化完成 - 日志目录: {self.logs_dir}") + + return root_logger + + def get_module_logger(self, module_name: str) -> logging.Logger: + return logging.getLogger(module_name) + + def create_component_logger(self, + component_name: str, + log_file: str = None, + log_level: int = None) -> logging.Logger: + logger = logging.getLogger(component_name) + + if log_file: + component_log_file = self.logs_dir / log_file + handler = RotatingFileHandler( + component_log_file, + maxBytes=5 * 1024 * 1024, + backupCount=3, + encoding='utf-8' + ) + + formatter = logging.Formatter(self.log_format, self.date_format) + handler.setFormatter(formatter) + + if log_level: + handler.setLevel(log_level) + + logger.addHandler(handler) + + return logger + + def setup_mcp_logging(self) -> logging.Logger: + return self.create_component_logger( + "mcp_services", + "mcp_services.log", + logging.DEBUG + ) + + def setup_api_logging(self) -> logging.Logger: + return self.create_component_logger( + "api_requests", + "api_requests.log", + logging.INFO + ) + + +# 全局日志配置实例 +logger_config = LoggerConfig() + + +def setup_system_logging(app_name: str = "lzwcai_workflow_to_mcp", + log_level: int = logging.INFO) -> logging.Logger: + return logger_config.setup_logging(app_name, log_level) + + +def get_logger(name: str) -> logging.Logger: + return logger_config.get_module_logger(name) diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/name_helper.py b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/name_helper.py new file mode 100644 index 0000000..7a5499d --- /dev/null +++ b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/name_helper.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +""" +名称生成工具模块 +""" + +from pypinyin import lazy_pinyin, Style +import logging + +logger = logging.getLogger(__name__) + + +def generate_tool_name(business_name: str, tool_id: str) -> str: + """ + 根据业务名称和ID生成工具名称 + 格式: tool_拼音_id + + Args: + business_name: 业务名称(中文) + tool_id: 工具ID + + Returns: + str: 格式化的工具名称 + """ + try: + # 将中文转换为拼音(无音调,小写) + pinyin_list = lazy_pinyin(business_name, style=Style.NORMAL) + # 拼接拼音 + pinyin_str = ''.join(pinyin_list) + + # 将 ID 中的 '-' 替换为 '_' + formatted_id = tool_id.replace('-', '_') + + # 组合成最终的工具名称 + tool_name = f"workflow_{pinyin_str}_{formatted_id}" + + return tool_name + except Exception as e: + logger.error(f"生成工具名称失败: {business_name}, {tool_id}, 错误: {e}", exc_info=True) + # 降级处理:如果拼音转换失败,使用 ID + return f"workflow_{tool_id.replace('-', '_')}" diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/schema_helper.py b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/schema_helper.py new file mode 100644 index 0000000..ee92867 --- /dev/null +++ b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/schema_helper.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +""" +Schema 生成工具模块 +""" + +from typing import Any, Dict, List + + +def generate_input_schema(parameters: Dict[str, Any]) -> Dict[str, Any]: + """ + 从查询配置的参数定义生成 MCP 工具的 inputSchema + + 此函数会保留完整的 JSON Schema 信息,包括: + - type: Schema 类型(通常是 "object") + - required: 必填字段列表 + - properties: 属性定义(包括每个属性的 type, description, format, examples 等) + - description: Schema 的整体描述(如果有) + - 以及其他任何 JSON Schema 标准字段 + + Args: + parameters: 查询配置中的参数定义字典,应该是一个完整的 JSON Schema 对象 + + Returns: + Dict[str, Any]: 符合 JSON Schema 规范的 inputSchema 对象 + """ + # 如果 parameters 本身就是一个完整的 JSON Schema 对象,直接使用 + # 但确保至少包含 type 和 properties + if not parameters: + # 如果 parameters 为空,返回一个空的 object schema + return { + "type": "object", + "properties": {}, + "required": [] + } + + # 深拷贝 parameters 以避免修改原始数据 + input_schema = dict(parameters) + + # 确保必需的字段存在 + if "type" not in input_schema: + input_schema["type"] = "object" + + if "properties" not in input_schema: + input_schema["properties"] = {} + + if "required" not in input_schema: + input_schema["required"] = [] + + return input_schema + + +def validate_input_schema(schema: Dict[str, Any]) -> tuple[bool, str]: + """ + 验证 inputSchema 是否符合基本的 JSON Schema 规范 + + Args: + schema: 要验证的 schema 对象 + + Returns: + tuple[bool, str]: (是否有效, 错误消息或成功消息) + """ + if not isinstance(schema, dict): + return False, "Schema 必须是一个字典对象" + + if schema.get("type") != "object": + return False, "Schema 的 type 字段必须是 'object'" + + if "properties" not in schema: + return False, "Schema 必须包含 properties 字段" + + if not isinstance(schema.get("properties"), dict): + return False, "Schema 的 properties 字段必须是一个字典对象" + + # 验证 required 字段(如果存在) + if "required" in schema: + required = schema["required"] + if not isinstance(required, list): + return False, "Schema 的 required 字段必须是一个列表" + + # 验证所有 required 的字段都在 properties 中定义 + properties = schema["properties"] + for field in required: + if field not in properties: + return False, f"必填字段 '{field}' 未在 properties 中定义" + + # 验证 properties 中每个字段的定义 + for prop_name, prop_def in schema["properties"].items(): + if not isinstance(prop_def, dict): + return False, f"属性 '{prop_name}' 的定义必须是一个字典对象" + + if "type" not in prop_def: + return False, f"属性 '{prop_name}' 必须包含 type 字段" + + return True, "Schema 验证通过" diff --git a/lzwcai_workflow_to_mcp/main.py b/lzwcai_workflow_to_mcp/main.py new file mode 100644 index 0000000..80107e5 --- /dev/null +++ b/lzwcai_workflow_to_mcp/main.py @@ -0,0 +1,16 @@ +""" +Entry point for lzwcai-workflow-to-mcp +Runs the MCP server for workflow execution +""" + +import os + +if __name__ == "__main__": + # 设置环境变量 + os.environ["workflowId"] = "2002300699510763521" + os.environ["workflowExecuteKey"] = "wf_buh230o9iaea4n6aefsddcexa7p27ydl" + os.environ["backendBaseUrl"] = "http://192.168.2.236:8088" + + # Import and run the actual MCP server + from lzwcai_workflow_to_mcp.main import main + main() diff --git a/lzwcai_workflow_to_mcp/pyproject.toml b/lzwcai_workflow_to_mcp/pyproject.toml new file mode 100644 index 0000000..3dd7ec5 --- /dev/null +++ b/lzwcai_workflow_to_mcp/pyproject.toml @@ -0,0 +1,35 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "lzwcai-workflow-to-mcp" +version = "0.1.8" +description = "MCP server for executing business SQL queries with dynamic tool generation" +readme = "README.md" +requires-python = ">=3.13" +license = {text = "MIT"} +authors = [ + {name = "lzwcai", email = "your-email@example.com"}, +] +keywords = ["mcp", "sql", "executor", "server"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "httpx>=0.28.1", + "mcp[cli]>=1.10.1", + "pypinyin>=0.53.0", +] + +[project.scripts] +lzwcai-workflow-to-mcp = "lzwcai_workflow_to_mcp.main:main" + +[tool.hatch.build.targets.wheel] +packages = ["lzwcai_workflow_to_mcp"] + +[tool.hatch.build.targets.wheel.force-include] +"lzwcai_workflow_to_mcp/businessQueries.json" = "lzwcai_workflow_to_mcp/businessQueries.json"