MCP实践示例

最近关于MCP(Model Context Protocol)的新闻挺多的。

但是这个MCP该如开发和使用呢?我因为种种原因用过MaxKB,

虽然MaxKB的新版说是支持MCP,但在TA的文档中并没有关于MCP该如何配置使用

的内容。那就只能自己摸索了.....

安装ollama

ollama是大模型推理服务平台,使用TA来跑大模型特别省事。

具体安装方法,请看:https://ollama.com/download

然后照做即可。我的ollama是安装到Uubntu系统上,

所以很多操作,默认操作系统是ubuntu。

安装大模型

大模型就选qwen2.5系的,qwen2.5个人感觉比较靠谱。

下载模型并跑起来:

ollama run qwen2.5:1.5b

配置ollama的可访问范围

ollama的接口默认的监听地址为:127.0.0.1:11434(仅本机能访问)
但为了方便,需要修改一下:

sudo mkdir -p /etc/systemd/system/ollama.service.d

编辑文件:

sudo nano /etc/systemd/system/ollama.service.d/env.conf

内容:

[Service]
Environment="OLLAMA_HOST=0.0.0.0"

重启ollama服务:

sudo systemctl daemon-reload
sudo systemctl restart ollama.service

这样ollama接口的监听地址就是:0.0.0.0:11434

安装MaxKB

MaxKB是个开源的大模型应用,安装方法参看:https://maxkb.cn/ 。就选新版就行了。

添加模型到MaxKB

系统设置 -> 模型设置 -> 添加模型 -> ollama -> 模型名称:qwen2.5:1.5b

-> 模型类型:大语言模型-> 基础模型:qwen2.5:1.5b

-> API URL:http://172.17.0.1:11434/ -> API Key:ollama -> 保存

API URL: 还需要根据ollama的实际部署情况填写

编写一个简单MCP服务

fastmcp

我们使用fastcmp来开发MCP服务比较省事

首先需要准备Python 3.10+的环境

安装

git clone https://github.com/jlowin/fastmcp.git
cd fastmcp
python3 -m pip install .

注意:

不要使用 ```python3 -m pip install fastmcp```, 因为有问题(2025-04-13).

代码

mcp_server.py:

#!/usr/bin/env python3

import os
import time
from fastmcp import FastMCP
import requests,json


# Create an MCP server
mcp = FastMCP("MCP-Server")

@mcp.tool()
def get_file(filename: str) -> str:
    """
    Get File Content
    :param filename file fullpath
    """
    txt = ''
    with open(filename,'r') as fp:
        txt = fp.read()
    print(filename,":",txt)
    return txt

@mcp.tool()
def get_weather(province,city,county=""):
    """
    获取天气情况
    :param province 省,自治区,直辖市,特别行政区
    :param city    城市名称
    :param county  县名,城市的区名
    """
    uri = f"https://wis.qq.com/weather/common?source=pc&weather_type=observe&province={province}&city={city}&county={county}"
    rsp = requests.get(uri)
    ret = {}
    if rsp.ok:
        ret = rsp.json()['data']['observe']
    ret = json.dumps(ret,ensure_ascii=False,indent=1)
    print(uri,":",ret)
    return ret

@mcp.tool()
def write_file(text):
    """
    把@text写入文件并返回文件名
    :param text 文本内容
    return: 文件路径
    """
    f = os.path.join("/tmp/",'{:.0f}.txt'.format(time.time()*1000))
    with open(f,'w') as fp:
        fp.write(text)
    ret = f"Text was written to {f}"
    print('```',text,'```',ret)
    return ret


if __name__ == '__main__':
    mcp.run(transport='sse')

MCP服务起来

fastmcp run -t sse mcp_server.py

创建MaxKB应用

MaxKB支持中英文, 为保持一致性, 操作时最好把浏览器语言支持, 调整为中文优先.

1

应用 -> 创建应用 -> 名称:MCP测试 -> 描述:试试MCP -> 类型:高级编排 -> 模板:空白应用 -> 创建

2

添加组件 -> 基础组件 -> AI 对话 -> AI 模型:qwen2.5:1.5b ->

提示词:{{开始.question}} -> 工具 -> MCP:开启 -> MCP Server Config:

{
    "MCP-Server":{
        "url":"http://172.17.0.1:8000/sse",
        "transport":"sse"
    }
}

-> 返回内容:开启 -> 连接组件 "开始" 到 "AI 对话", 如图:
start_to_aichat.png

测试

完成应用创建后,就试一下(保存 -> 调试) 如图:
debug.png

然后就试试看看,MCP服务相关函数是否能被成功被调用, 一些结果如下:
get_weather_0.png

get_weather_1.png

get_file.png

write_file.png

结束语

MCP的服务能否被调用有点靠运气,应该调用MCP,但不调用(或调用了参数错误)的感受特别明显.

在Ollama上的大语言模型,目前只发现qwen2.5系在MaxKB上可以成功调用MCP服务的功能/函数.

特别火的DeepSeek也试了, 没能成功的调用MCP服务的函数, MCP还没有被广泛支持.

参考链接

https://blog.csdn.net/qq_34786108/article/details/141857870

https://maxkb.cn/docs/

https://ollama.com/

使用LD_PRELOAD保护个人信息

引言

在现在这个时代,想要个人信息完全不暴露,几乎是不可能的。

不管用的哪个软件,只要TA跑起来,TA就可以读取电脑上的很多文件。

那么在Uubntu上有没有什么办法,可以控制程序一些文件能读而另一些文件不能读呢?

其实是有的。比如:文件权限,但文件权限控制太僵硬了。有时不给读,程序就不起来。

我们这个文章就介绍一种比较温柔的方法:允许程序读,但是对于某些文件读取的内容是脱敏后的。

LD_PRELOAD

在ubuntu系统,程序读取文件一般都是先调用(直接或间接)open或fopen函数来打开文件。

int open(const char *pathname, int flags, ... /* mode_t mode */ );
FILE *fopen(const char *restrict pathname, const char *restrict mode);

只要我们想办法,让在程序调用open或fopen前,

修改一下pathname的路径,就可以把程序要读取的文件改成其它文件。

从而保护原来文件不被读取。

而ld.so正好提供环境变量LD_PRELOAD,可以帮助我们实现上述功能。

LD_PRELOAD列的共享库文件(.so)中的函数,将被程序优先调用。

所以我们需要作的就是,自己实现一个版本open和fopen函数。TA们的功能是:

当被调用时,检查一下pathname是否为敏感文件,如果是则修改pathname为不敏感版本的文件,

然后再调用原始版本open和fopen打开文件返回。

代码(path_rename.c)参考如下:

#include <dlfcn.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdarg.h>

extern int vsnprintf(char *str, size_t size, const char *format, va_list ap);
extern int snprintf(char *str, size_t size,
                   const char *restrict format, ...);

static inline ssize_t print(const char*fmt,...){
    char buf[4096] = {0};
    va_list ap;

    va_start(ap, fmt);
    int n = vsnprintf(buf, sizeof(buf), fmt, ap);
    va_end(ap);

    return write(2,buf,n);
}

static const char *PATH_MAP[][2] = {
    /**
    {"需要保护的文件路径","实际上允许读取的文件路径"}
    **/
    { "/etc/lsb-release", "/etc/lsb-release.pub" },
    { "/etc/issue", NULL},/** NULL -> /etc/issue.pricacy **/
    { "for_test", NULL}, /** NULL -> for_test.pricacy **/
};

static const int PATH_MAP_LEN = sizeof(PATH_MAP) / sizeof(PATH_MAP[0]);

static inline void fix_path(const char*path,char *out_path,size_t out_len)
{
        if(out_len<0)return;
        out_path[0] = 0;
        if(path == NULL)return;

        //默认:维持pathname路径不变
        strncpy(out_path,path,out_len);

        for(int i=0;i<PATH_MAP_LEN;i++){
                const char*o = PATH_MAP[i][0];
                if(o == NULL)continue;
                const char*n = PATH_MAP[i][1];
                if(strcmp(path,o) == 0){
                        if(n == NULL){
                                snprintf(out_path,out_len,"%s.privacy",path);
                        }else{
                                strncpy(out_path,n,out_len);
                        }
                        print("%s -> %s\n",o,out_path);
                        break;
                }
        }
}

int open (const char * pathname, int flags, ...)
{
    print("%s:%s(%s,%d)\n",__FILE__,__func__,pathname,flags);

    char path[4096] = {0};
    fix_path(pathname,path,sizeof(path));
    static int (*open_ptr)(const char* , int) = NULL;
    if(open_ptr == NULL){
        open_ptr = dlsym(RTLD_NEXT,__func__);
    }
    int fd = open_ptr(path,flags);
    return fd;
}

void *fopen(const char *pathname, const char *mode){
    print("%s:%s(%s,%d)\n",__FILE__,__func__,pathname,mode);
    char path[4096] = {0};
    fix_path(pathname,path,sizeof(path));
    static void* (*open_ptr)(const char* , const char *) = NULL;
    if(open_ptr == NULL){
        open_ptr = dlsym(RTLD_NEXT,__func__);
    }
    void* fd = open_ptr(path,mode);
    return fd;
}

我们把新实现open和fopen编译成共享库(path_rename.so):

gcc -shared -fpic path_rename.c -o path_rename.so -ldl -D_GNU_SOURCE

写个简单代码(main.c),用来测试:

#include <stdio.h>

int main(int argc,char **argv)
{
    if(argc>1){
        FILE *fp = fopen(argv[1],"rb");
        if(fp){
            char buf[4097] = {0};
            size_t n = 0;
            while(!feof(fp)){
                n = fread(buf,1,sizeof(buf)-1,fp);
                //printf("n: %ld\n",n);
                buf[n] = 0;
                printf("%s",buf);
            }
            fclose(fp);
        }
    }
    return 0;
}

再写个简单Makefile:

main:main.c
    $(CC) $< -o $@  -g3 -Wall

path_rename.so:path_rename.c
    $(CC) -shared -fpic $< -o $@ -ldl -D_GNU_SOURCE

clean:
    rm -rf *.so main

test:main path_rename.so Makefile
    LD_PRELOAD=./path_rename.so ./main main.c || true
    LD_PRELOAD=./path_rename.so cat for_test /etc/lsb-release || true

然后

make test

就可以立马看到效果了。

用法

修改path_rename.c中的PATH_MAP,再

make path_rename.so

然后

LD_PRELOAD=dir_of_so/path_rename.so xxxx

这样程序xxxx,能读什么文件就随我们拿捏了。

注意

函数open和fopen,只是比较常见的用于打开文件的函数。

打开文件还有其TA函数可选择,具体可自行查抄。

但针对其TA函数,实现文件保护的实现套路是一样一样的。

Ubuntu 18.04 在root文件上,系统启用lvmcache

虽然18.04有点老,但是还有再用。且还遇到了问题,所以就写写

1 root文件系统必须是lvm的逻辑卷

具体如何创建lvm逻辑卷,搜一下,应该很容易能找到。所以这里省略了......

2 /boot使用独立分区

/boot分区一般给个500+MiB就差不多够用了。
为什么要这样呢?据说是因grub读不了lvmcache(我没试过,但其他人的说法我信了),
如果不独立分区的话,/boot中的内核、grub、initrd就都在lvmcache上了,
grub无法读取这些数据,引导不了系统。

3 安装必要程序

    sudo apt install -y thin-provisioning-tools

4 启用lvmcache

首先保存如下代码:
#!/bin/bash

ssd=$2 # pv 用于作为缓存,存储空间(可以整个磁盘/磁盘分区)
vg=$(basename $(dirname "$3"))
lv=$(basename "$3")

function cache(){
    local ssd_size meta_size cache_size
    ssd_size=$(pvs ${ssd} --units m|grep -v PSize |awk '{print $5}'|sed 's|\.[0-9]*m||g')
    meta_size=$(((${ssd_size}/800)*8))
    cache_size=$(( ((${ssd_size}-${meta_size}*2)/8)*8 ))

    vgextend ${vg} ${ssd}
    #lvcreate -n cache -L ${cache_size}MiB ${vg} ${ssd} && \
    #lvcreate -n cachemeta -L ${meta_size}MiB ${vg} ${ssd} && \
    #lvconvert --type cache-pool --poolmetadata ${vg}/cachemeta ${vg}/cache && \
    #lvconvert --type cache --cachepool ${vg}/cache --cachemode writeback ${vg}/$lv
    lvcreate --type cache-pool -n cache -L ${cache_size}MiB ${vg} ${ssd} && \
    lvconvert --type cache --cachepool ${vg}/cache --cachemode writeback ${vg}/$lv
}

function vg_name()
{
    pvdisplay "$1"|grep 'VG Name'|awk '{print $3}'
}

function uncache()
{
    vg0=$(vg_name ${ssd})
    test "x${vg0}" = "x${vg}" || {
        echo "bad pv"
        exit 1
    }
    lvconvert --uncache ${vg}/$lv && {
        lvremove ${vg}/cachemeta
        lvremove ${vg}/cache
        vgreduce ${vg} ${ssd}
    }
}

function usage()
{
    echo "$0 [cache | uncache] cache_pv vg/lv"
    exit 0
}

( test -z "$1" || test -z "$2" || test -z "$vg" || test "$vg" = "." || test -z "$lv" || test "$lv" = "." ) && usage

lvp=/dev/${vg}/${lv}
test -b ${lvp} || {
    echo "$lvp is not exist"
    exit 1
}

test -b ${ssd} || {
    echo "$ssd is not exist"
    exit 2
}

$1
假设代码保存为lvmcache,挂在到root文件系统的lvm逻辑卷为/dev/root/ubuntu,
用于缓存的磁盘分区为/dev/sddX,为/dev/root/ubuntu,启用cache:
    bash lvmcache cache /dev/sddX /dev/root/ubuntu
然后一路按'y'就可以了。

5 更新initrd

因为ubuntu 18.04的thin-provisioning-tools程序包,
没自动配置initrd支持lvmcache。所以我们需要手动搞一下,代码如下(copy from 23.10):
#!/bin/sh

PREREQ=""

prereqs()
{
    echo "$PREREQ"
}

case $1 in
prereqs)
    prereqs
    exit 0
    ;;
esac

. /usr/share/initramfs-tools/hook-functions

copy_exec /usr/sbin/pdata_tools
ln -s pdata_tools ${DESTDIR}/usr/sbin/cache_check
ln -s pdata_tools ${DESTDIR}/usr/sbin/thin_check

manual_add_modules dm-cache
manual_add_modules dm-cache-smq
manual_add_modules dm-thin-pool
保存到:/usr/share/initramfs-tools/hooks/thin-provisioning-tools
更新initrd,命令如下:
sudo update-initramfs -k all -u -t

重启系统

如果能够顺利,回归系统,说明root文件系统启用lvmcache成功了。

Ubuntu系统,如何让dkms编译的内核模块支持安全启动?

1 在BIOS中打开SecureBoot支持

这因为不同品牌的计算机设置不尽相同,就不做详细说明了。
根据自己计算机的实际情况,搜一下基本上都能找方法。
或者找一下购买计算机附赠的手册,一般也能找到开启/关闭安全启动的方法

在Ubuntu系统中,查看SecureBoot的状态:

mokutil --sb-state

2 安装必要软件包

sudo apt-get install --yes shim-signed mokutil

3 创建签名内核模块需要的密钥

sudo update-secureboot-policy --new-key

生成了两文件:

ls /var/lib/shim-signed/mok/
MOK.der  MOK.priv

MOK.der --- 公钥

MOK.priv --- 密钥

4 公钥导到安全启动的信任列表

sudo mokutil --import /var/lib/shim-signed/mok/MOK.der

这一步会要求设置一个密码,可随意设个简单密码就行。

注意:这个命令仅是做一个“导入密钥的计划”,真正导入需要重启一下,进行实操。

5 重启计算机导入公钥

重启后就进入了一个操作界面,分别选择:

Enroll MOK -> continue -> yes -> password -> reboot

password 就是上一步import时,设置的密码

注意:

如果这个环节有哪一步选错了,可以重复4,5

6 内核模块签名

搞了上面的操作之后,dkms再编译完内核模块,

就会自动使用/var/lib/shim-signed/mok下的密钥,

对生成的内核模块进行签名

所以只需要重新编译一些内核模块即可:

for m in $(dkms status |sed 's|[/:]|,|g;s|[ \t]||g' |awk -F, '{print $1"/"$2}' |sort |uniq)
do
    dkms remove ${m} --all
    for v in /lib/modules/*
    do
        test -d "${v}/initrd" || continue
        v=$(basename $v)
        dkms install ${m} -k ${v}
    done
done

7 完成

此后,就用dkms辅助编译内核模块(自动签名),SecureBoot不再是个麻烦。
例如:
    sudo apt-get install --yes nvidia-driver-xxx
    sudo apt-get install --yes virtualbox

docker build 遇到:

* xxx (Y/I/N/O/D/Z) [default=N] ? dpkg: error processing package xxx (--configure):
end of file on stdin at conffile prompt

怎么办?
给apt-get install 或 apt-get upgrade,添加参数

-o Dpkg::Options::="--force-confold"

强制保留当前的配置文件即可