@mfukushim/map-traveler-mcp
Version:
Virtual traveler library for MCP
919 lines (918 loc) • 43.5 kB
JavaScript
/*! map-traveler-mcp | MIT License | https://github.com/mfukushim/map-traveler-mcp */
import { Effect, Option } from "effect";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { defaultAvatarId, RunnerService, RunnerServiceLive, useAiImageGen } from "./RunnerService.js";
import { MapService, MapServiceLive } from "./MapService.js";
import { __pwd, DbService, DbServiceLive, env } from "./DbService.js";
import { StoryService, StoryServiceLive } from "./StoryService.js";
import { FetchHttpClient, HttpClient } from "@effect/platform";
import { defaultBaseCharPrompt, ImageService, ImageServiceLive } from "./ImageService.js";
import { logSync, McpLogService, McpLogServiceLive } from "./McpLogService.js";
import { AnswerError } from "./mapTraveler.js";
import { SnsService, SnsServiceLive } from "./SnsService.js";
import * as path from "path";
import * as fs from "node:fs";
import dayjs from "dayjs";
import { bodyAreaRatio, bodyHWRatio, bodyWindowRatioH, bodyWindowRatioW, bs_handle, noImageOut } from "./EnvUtils.js";
const feedTag = "#geo_less_traveler";
const feedUri = "at://did:plc:ygcsenazbvhyjmxeltz4fgw4/app.bsky.feed.generator/marble_square25";
const LabelGoogleMap = 'Google Map';
const LabelClaude = 'Claude';
const labelImage = (aiGen) => {
return aiGen === 'pixAi' ? 'PixAi' : aiGen === 'sd' ? 'Stability.ai' : '';
};
const server = new Server({
name: "geo-less-traveler",
version: "0.1.0",
}, {
capabilities: {
resources: {
listChanged: true
},
tools: {
listChanged: true
},
prompts: {},
sampling: {},
},
});
export function sendProgressNotification(progressToken, total, progress) {
return Effect.tryPromise({
try: () => server.notification({
method: "notifications/progress",
params: {
progress: progress,
total: total,
progressToken,
},
}),
catch: error => {
logSync(`sendProgressNotification catch:${error}`);
return new Error(`sendProgressNotification error:${error}`);
}
}).pipe(Effect.tap(() => logSync('do sendProgressNotification')));
}
function sendToolListChanged() {
logSync('start sendToolListChanged');
return Effect.tryPromise({
try: () => server.sendToolListChanged(),
catch: error => {
McpLogService.logError(`sendToolListChanged error:${error}`);
return new Error(`sendToolListChanged error:${error}`);
}
}).pipe(Effect.tap(() => logSync('sendToolListChanged put')));
}
function sendLoggingMessage(message, level = 'info') {
return Effect.tryPromise({
try: () => server.sendLoggingMessage({
level: level,
data: message,
}),
catch: error => {
McpLogService.logError(`sendLoggingMessage catch:${error}`);
return new Error(`sendLoggingMessage error:${error}`);
}
});
}
export class McpService extends Effect.Service()("traveler/McpService", {
accessors: true,
effect: Effect.gen(function* () {
const SETTING_COMMANDS = [
{
name: "tips",
description: "Inform you of recommended actions for your device",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "get_setting",
description: "Get current setting",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "get_traveler_info",
description: "get a traveler's setting.For example, traveler's name, the language traveler speak, Personality and speaking habits, etc.",
inputSchema: {
type: "object",
properties: {
settings: {},
}
}
},
{
name: "set_traveler_info",
description: "set a traveler's setting.For example, traveler's name, the language traveler speak, Personality and speaking habits, etc.",
inputSchema: {
type: "object",
properties: {
settings: {
type: "string",
description: "traveler's setting. traveler's name, the language traveler speak, etc."
},
},
required: ["settings"]
}
},
];
const AVATAR_PROMPT_COMMANDS = [
{
name: "set_avatar_prompt",
description: "set a traveler's avatar prompt. A prompt for AI image generation to specify the appearance of a traveler's avatar",
inputSchema: {
type: "object",
properties: {
prompt: {
type: "string",
description: "traveler's avatar AI image generation prompt."
},
},
required: ["prompt"]
}
},
{
name: "reset_avatar_prompt",
description: "reset to default traveler's avatar prompt.",
inputSchema: {
type: "object",
properties: {},
}
},
];
const START_STOP_COMMAND = [
{
name: env.personMode === 'second' ? "start_journey" : "start_traveler_journey",
description: env.personMode === 'second' ? "Start the journey to destination" : "Start the traveler's journey to destination",
inputSchema: {
type: "object",
properties: {},
}
},
{
name: env.personMode === 'second' ? "stop_journey" : "stop_traveler_journey",
description: env.personMode === 'second' ? "Stop the journey" : "Stop the traveler's journey",
inputSchema: {
type: "object",
properties: {},
}
},
];
const SKIP_COMMAND = [
{
name: "reach_a_percentage_of_destination",
description: "Reach a specified percentage of the destination",
inputSchema: {
type: "object",
properties: {
timeElapsedPercentage: {
type: "number",
description: "Percent progress towards destination. (0~100)"
},
},
required: ["timeElapsedPercentage"]
}
},
];
const GET_VIEW_COMMAND = [
{
name: env.personMode === 'second' ? "get_current_view_info" : "get_traveler_view_info",
description: env.personMode === 'second' ?
"Get the address of the current location and information on nearby facilities,view snapshot" :
"Get the address of the current traveler's location and information on nearby facilities,view snapshot",
inputSchema: {
type: "object",
properties: {
includePhoto: {
type: "boolean",
description: "Get scenery photos of current location"
},
includeNearbyFacilities: {
type: "boolean",
description: "Get information on nearby facilities"
},
}
}
},
{
name: "get_traveler_location",
description: "Get the address of the current traveler's location",
inputSchema: {
type: "object",
properties: {}
}
},
];
const SNS_COMMAND = [
{
name: "get_sns_mentions",
description: "Get recent social media mentions",
inputSchema: {
type: "object",
properties: {},
}
},
{
name: "get_sns_feeds",
description: "Get recent social media posts from fellow travelers feeds",
inputSchema: {
type: "object",
properties: {},
}
},
{
name: "post_sns_writer",
description: "Post your recent travel experiences to social media for fellow travelers and readers.",
inputSchema: {
type: "object",
properties: {
message: {
type: "string",
description: "A description of the journey. important: Do not use offensive language."
}
},
required: ["message"]
}
},
{
name: "reply_sns_writer",
description: "Write a reply to the article with the specified ID.",
inputSchema: {
type: "object",
properties: {
message: {
type: "string",
description: "A description of the reply article. important: Do not use offensive language."
},
id: {
type: "string",
description: "The ID of the original post to which you want to add a reply."
}
},
required: ["message", "id"]
}
},
{
name: "add_like",
description: "Add a like to the specified post",
inputSchema: {
type: "object",
properties: {
id: {
type: "string",
description: "The ID of the post to like."
}
},
required: ["id"]
}
},
];
const makeToolsDef = () => {
const def = () => {
if (env.isPractice) {
return [
...GET_VIEW_COMMAND,
...SETTING_COMMANDS,
...START_STOP_COMMAND
];
}
else {
const basicToolsCommand = [
{
name: env.personMode === 'second' ? "set_current_location" : "set_traveler_location",
description: env.personMode === 'second' ? "Set my current address" : "Set the traveler's current address",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: env.personMode === 'second' ? "address to set" : "address set to traveler"
}
},
required: ["address"]
}
},
{
name: env.personMode === 'second' ? "get_destination_address" : "get_traveler_destination_address",
description: env.personMode === 'second' ? "get a address of destination location" : "get a address of traveler's destination location",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: env.personMode === 'second' ? "set_destination_address" : "set_traveler_destination_address",
description: env.personMode === 'second' ? "set a address of destination" : "set a address of traveler's destination",
inputSchema: {
type: "object",
properties: {
address: {
type: "string",
description: "address of destination"
}
},
required: ["address"]
}
},
];
const cmd = [];
if (env.travelerExist) {
cmd.push(...basicToolsCommand, ...GET_VIEW_COMMAND, ...SETTING_COMMANDS);
if (env.moveMode === "skip") {
cmd.push(...SKIP_COMMAND);
}
else {
cmd.push(...START_STOP_COMMAND);
}
if (env.anySnsExist) {
cmd.push(...SNS_COMMAND);
}
if (!env.fixedModelPrompt) {
cmd.push(...AVATAR_PROMPT_COMMANDS);
}
}
else {
cmd.push({
name: "call_traveler",
description: "call the traveler",
inputSchema: {
type: "object",
properties: {}
}
});
}
return cmd;
}
};
return Effect.succeed(def()).pipe(Effect.andThen(a => ({ tools: env.filterTools.length > 0 ? a.filter(b => env.filterTools.includes(b.name)) : a })));
};
const tips = () => {
return Effect.gen(function* () {
const res = yield* StoryService.tips();
const content = [{ type: "text", text: res.textList.join('\n-------\n') }];
if (res.imagePathList.length > 0) {
yield* Effect.forEach(res.imagePathList, a => {
return Effect.async((resume) => fs.readFile(path.join(__pwd, a), (err, data) => {
if (err) {
resume(Effect.fail(err));
}
resume(Effect.succeed(data));
})).pipe(Effect.andThen(b => {
content.push({
type: "image",
data: Buffer.from(b).toString('base64'),
mimeType: 'image/png'
});
}));
});
}
return content;
}).pipe(Effect.provide(StoryServiceLive));
};
const setPersonMode = (person) => {
const mode = person === 'second_person' ? 'second' : 'third';
env.personMode = mode;
return DbService.saveEnv('personMode', mode).pipe(Effect.tap(() => sendToolListChanged()), Effect.andThen(a => [{ type: "text", text: `Person mode set as follows: ${a.value}` }]));
};
const getTravelerInfo = () => {
return DbService.getEnv('aiEnv').pipe(Effect.andThen(a => [{
type: "text",
text: `The traveller's information is as follows: ${a}`
}]), Effect.orElseSucceed(() => [{
type: "text",
text: `There is no traveler information.`
}]));
};
const setTravelerInfo = (info) => {
return DbService.saveEnv('aiEnv', info).pipe(Effect.andThen(a => [{
type: "text",
text: `The traveller information is as follows: ${a.value}`
}]));
};
const getSetting = () => {
return Effect.gen(function* () {
const version = yield* DbService.getVersion();
const envText = 'A json of current environment settings\n' +
Object.entries(env).map(([key, value]) => {
return `${key}= ${JSON.stringify(value)}`;
}).join('\n') +
'\nList of Image settings\n' +
(bodyAreaRatio ? `bodyAreaRatio=${bodyAreaRatio}\n` : '') +
(bodyHWRatio ? `bodyHWRatio=${bodyHWRatio}\n` : '') +
(bodyWindowRatioW ? `bodyWindowRatioW=${bodyWindowRatioW}\n` : '') +
(bodyWindowRatioH ? `bodyWindowRatioH=${bodyWindowRatioH}\n` : '') +
(noImageOut ? `noImage=true\n` : '') +
`version=${version}\n`;
return [{
type: "text",
text: envText
}];
});
};
const setAvatarPrompt = (prompt) => {
return DbService.updateBasePrompt(defaultAvatarId, prompt).pipe(Effect.andThen(a => [{
type: "text",
text: `Set traveller prompt to: "${a}"`
}]), Effect.tap(() => {
env.promptChanged = true;
return DbService.saveEnv('promptChanged', '1');
}));
};
const resetAvatarPrompt = () => {
return DbService.updateBasePrompt(defaultAvatarId, defaultBaseCharPrompt).pipe(Effect.andThen(() => [{
type: "text",
text: `reset traveller prompt to default.`
}]));
};
const getCurrentLocationInfo = (includePhoto, includeNearbyFacilities) => {
return RunnerService.getCurrentView(dayjs(), includePhoto, includeNearbyFacilities, env.isPractice).pipe(Effect.provide([MapServiceLive, DbServiceLive, StoryServiceLive, RunnerServiceLive, FetchHttpClient.layer, ImageServiceLive]));
};
const getElapsedView = (timeElapsedPercentage) => {
if (env.isPractice) {
return practiceNotUsableMessage;
}
return RunnerService.getElapsedView(timeElapsedPercentage).pipe(Effect.provide([MapServiceLive, DbServiceLive, StoryServiceLive, RunnerServiceLive, FetchHttpClient.layer, ImageServiceLive]));
};
const practiceNotUsableMessage = Effect.succeed([
{
type: "text",
text: 'Sorry,this feature not usable in practice mode. ' +
'Please assign Google Map API key and restart application if you use this function. ' +
'For more information, please see the resource "SettingInfo"'
}
]);
const setCurrentLocation = (location) => {
if (env.isPractice) {
return practiceNotUsableMessage;
}
if (!location) {
throw new AnswerError("Location address is required");
}
return Effect.gen(function* () {
const address = yield* MapService.getMapLocation(location);
if (Option.isNone(address)) {
return yield* Effect.fail(new AnswerError("I don't know where you're talking about. location not found"));
}
const timeZoneId = yield* MapService.getTimezoneByLatLng(address.value.lat, address.value.lng);
yield* McpLogService.logTrace(address.value);
yield* DbService.saveRunStatus({
id: 1,
status: 'stop',
from: '',
to: address.value.address,
destination: null,
startLat: 0,
startLng: 0,
endLat: address.value.lat,
endLng: address.value.lng,
durationSec: 0,
distanceM: 0,
avatarId: 1,
tripId: 0,
tilEndEpoch: 0,
startTime: new Date(0),
endTime: new Date(0),
startCountry: address.value.country,
endCountry: address.value.country,
startTz: timeZoneId,
endTz: timeZoneId,
currentPathNo: -1,
currentStepNo: -1,
});
const setMessage = [
`location set succeed`,
`address:${address.value.address}`,
`latitude:${address.value.lat}, longitude:${address.value.lng}`
];
yield* DbService.getEnv('destination').pipe(Effect.andThen(dest => RunnerService.setDestinationAddress(dest)), Effect.andThen(a => setMessage.push(a.message)), Effect.orElse(() => Effect.succeed(true)));
return [{
type: "text",
text: setMessage.join('\n')
}];
}).pipe(Effect.provide([MapServiceLive, DbServiceLive, RunnerServiceLive]));
};
const getDestinationAddress = () => {
if (env.isPractice) {
return practiceNotUsableMessage;
}
return RunnerService.getDestinationAddress().pipe(Effect.andThen(a => [{ type: "text", text: `Current destination is "${a}"` }]), Effect.provide([MapServiceLive, StoryServiceLive, RunnerServiceLive, ImageServiceLive, DbServiceLive]));
};
const setDestinationAddress = (address) => {
if (env.isPractice) {
return practiceNotUsableMessage;
}
return RunnerService.setDestinationAddress(address).pipe(Effect.andThen(a => [{ type: "text", text: a.message }]), Effect.provide([MapServiceLive, DbServiceLive, StoryServiceLive, RunnerServiceLive, ImageServiceLive]));
};
const startJourney = () => {
return RunnerService.startJourney(env.isPractice).pipe(Effect.andThen(a => {
const out = [{ type: "text", text: a.text }];
if (Option.isSome(a.image)) {
out.push({
type: "image",
data: a.image.value.toString("base64"),
mimeType: 'image/png'
});
}
return out;
}), Effect.provide([MapServiceLive, DbServiceLive, StoryServiceLive, RunnerServiceLive, FetchHttpClient.layer, ImageServiceLive]));
};
const stopJourney = () => {
return RunnerService.stopJourney(env.isPractice).pipe(Effect.provide([MapServiceLive, DbServiceLive, StoryServiceLive, RunnerServiceLive, FetchHttpClient.layer, ImageServiceLive]));
};
const setTravelerExist = (callKick) => {
env.travelerExist = callKick;
return McpLogService.log(`enter setTravelerExist:${callKick}`).pipe(Effect.tap(() => sendToolListChanged()), Effect.andThen(() => {
return [{
type: "text",
text: (callKick ? "traveler called" : "traveler kicked")
}];
}));
};
const makeVisitorMessage = (notification) => {
return Effect.gen(function* () {
const visitorProf = yield* SnsService.getProfile(notification.handle);
const recentvisitorPosts = yield* SnsService.getAuthorFeed(notification.handle, 3);
const mentionPostText = yield* SnsService.getPost(notification.uri).pipe(Effect.andThen(a => Effect.succeed(Option.fromNullable(a.posts.length > 0 ? a.posts[0].record.text : undefined))));
const repliedPostText = notification.mentionType === 'reply' && notification.parentUri ?
(yield* SnsService.getPost(notification.parentUri).pipe(Effect.andThen(a => Effect.succeed(Option.fromNullable(a.posts.length > 0 ? a.posts[0].record.text : undefined))))) : Option.none();
const visitorName = notification.name || notification.handle || '誰か';
yield* McpLogService.logTrace(`avatarName:${visitorName}`);
let recentVisitorPost = '';
let recentVisitorPostId = '';
if (recentvisitorPosts && recentvisitorPosts.feed.length > 0) {
const p = recentvisitorPosts.feed[0].post;
recentVisitorPost = p.record.text;
recentVisitorPostId = p.uri + '-' + p.cid;
}
yield* McpLogService.logTrace(`mentionPostText:${Option.getOrUndefined(mentionPostText)}`);
yield* McpLogService.logTrace(`repliedPostText:${Option.getOrUndefined(repliedPostText)}`);
yield* McpLogService.logTrace(`visitorPostText:${recentVisitorPost}`);
return {
visitorName,
recentVisitorPost: recentVisitorPost,
visitorProf: visitorProf.description,
mentionPost: mentionPostText,
repliedPost: repliedPostText,
target: notification.mentionType === 'reply' ? notification.uri + '-' + notification.cid :
recentVisitorPost.includes(feedTag) ? recentVisitorPostId : notification.uri + '-' + notification.cid
};
});
};
const getSnsMentions = () => {
return Effect.gen(function* () {
const notifications = yield* SnsService.getNotification();
const { reply, like } = notifications.reduce((p, c) => {
if (c.mentionType === 'reply') {
p.reply.push(c);
}
else if (c.mentionType === 'like') {
p.like.push(c);
}
return p;
}, { reply: [], like: [] });
const likeMes = yield* Effect.forEach(like, a => makeVisitorMessage(a));
const replyMes = yield* Effect.forEach(reply.filter(v => v.rootUri != v.parentUri), a => makeVisitorMessage(a));
const likeText = `Our SNS post received the following likes.\n` +
`|id|Name of the person who liked the post|recent article by the person who liked this|Profile of the person who liked|\n` +
likeMes.map((a) => `|"${a.target}"|${a.visitorName}|${a.recentVisitorPost}|${a.visitorProf}|`).join('\n') +
'\n';
const replyText = `We received the following reply to our SNS post:\n` +
`|id|The name of the person who replied|Content of the reply article|Profile of the person who replied|\n` +
replyMes.map((a) => `|"${a.target}"|${a.visitorName}|${Option.getOrElse(a.mentionPost, () => '')}|${a.visitorProf || ''}|`).join('\n') +
'\n';
const content = [];
if (replyMes.length > 0) {
content.push({
type: 'text',
text: replyText
});
}
if (likeMes.length > 0) {
content.push({
type: 'text',
text: likeText
});
}
return content;
}).pipe(Effect.provide(SnsServiceLive));
};
const readSnsReader = () => {
return Effect.succeed([]);
};
const getSnsFeeds = () => {
return Effect.gen(function* () {
const posts = yield* SnsService.getFeed(feedUri, 4);
const detectFeeds = posts.filter(v => v.post.author.handle !== bs_handle)
.reduce((p, c) => {
if (!p.find(v => v.post.author.handle === c.post.author.handle)) {
p.push(c);
}
return p;
}, []);
const select = detectFeeds.map(v => {
const im = v.post.embed?.images;
return ({
id: v.post.uri + '-' + v.post.cid,
authorHandle: v.post.author.displayName || v.post.author.handle,
body: v.post.record.text || '',
imageUri: im ? im[0].thumb : undefined
});
});
const out = select.map(v => `id: ${v.id} \nauthor: ${v.authorHandle}\nbody: ${v.body}`).join('\n-----\n');
const images = select.flatMap(v => v.imageUri ? [{ uri: v.imageUri, handle: v.authorHandle }] : []);
const imageOut = yield* Effect.forEach(images, (a) => {
return HttpClient.get(a.uri).pipe(Effect.andThen((response) => response.arrayBuffer), Effect.scoped, Effect.provide(FetchHttpClient.layer)).pipe(Effect.andThen(a1 => ({
type: "image",
data: Buffer.from(a1).toString("base64"),
mimeType: "image/jpeg"
})));
});
const c = [{
type: 'text',
text: `I got the following article:\n-----\n${out}\n-----\n`
}];
c.push(...imageOut);
return c;
}).pipe(Effect.provide(SnsServiceLive));
};
const addLike = (id) => {
return Effect.gen(function* () {
if (env.noSnsPost) {
const noMes = [
{
type: "text",
text: env.loggingMode ? "like to log." : "Like to SNS is stopped by env settings."
}
];
if (env.loggingMode) {
return yield* McpLogService.logTrace(`log like:${id}`).pipe(Effect.andThen(() => noMes));
}
return yield* Effect.succeed(noMes);
}
const exec = /^"?(at.+?)"?$/.exec(id);
const id2 = exec && exec[1];
if (!id2) {
return yield* Effect.fail(new AnswerError('id is invalid'));
}
return yield* SnsService.addLike(id2).pipe(Effect.andThen(() => [
{
type: "text",
text: "Liked"
}
]), Effect.provide(SnsServiceLive));
});
};
const replySnsWriter = (message, id) => {
return Effect.gen(function* () {
if (env.noSnsPost) {
const noMes = [
{
type: "text",
text: env.loggingMode ? "posted to log." : "Posting to SNS is stopped by env settings."
}
];
if (env.loggingMode) {
return yield* McpLogService.log(message).pipe(Effect.andThen(() => noMes));
}
return yield* Effect.succeed(noMes);
}
const exec = /^"?(at.+?)"?$/.exec(id);
const id2 = exec && exec[1];
if (!id2) {
return yield* Effect.fail(new AnswerError('id is invalid'));
}
const appendLicence = 'powered ' + LabelClaude;
return yield* SnsService.snsReply(message.replaceAll("@", ""), `${appendLicence} ${feedTag} `, id2).pipe(Effect.andThen(() => [
{
type: "text",
text: "posted"
}
]), Effect.provide(SnsServiceLive));
});
};
const postSnsWriter = (message) => {
return Effect.gen(function* () {
if (env.noSnsPost) {
const noMes = [
{
type: "text",
text: env.loggingMode ? "posted to log." : "Posting to SNS is stopped by env settings."
}
];
return yield* Effect.succeed(noMes).pipe(Effect.tap(() => env.loggingMode && McpLogService.logTrace(message)));
}
const img = yield* ImageService.getRecentImageAndClear().pipe(Effect.tap(a => McpLogService.logTrace(`sns image:${a !== undefined}`)));
const recentUseLabels = [LabelClaude];
if (useAiImageGen) {
recentUseLabels.push(LabelGoogleMap, labelImage(useAiImageGen));
}
const appendLicence = 'powered ' + recentUseLabels.join(',');
return yield* SnsService.snsPost(message.replaceAll("@", ""), `${appendLicence} ${feedTag} `, img ? {
buf: img,
mime: "image/png"
} : undefined).pipe(Effect.andThen(() => [
{
type: "text",
text: "posted"
}
]), Effect.provide(SnsServiceLive));
});
};
const toolSwitch = (request) => {
switch (request.params.name) {
case "tips":
return tips();
case "get_traveler_info":
return getTravelerInfo();
case "set_traveler_info":
return setTravelerInfo(String(request.params.arguments?.settings));
case "get_setting":
return getSetting();
case "set_avatar_prompt":
return setAvatarPrompt(String(request.params.arguments?.prompt));
case "reset_avatar_prompt":
return resetAvatarPrompt();
case "get_current_view_info":
case "get_traveler_view_info":
return getCurrentLocationInfo(request.params.arguments?.includePhoto, request.params.arguments?.includeNearbyFacilities);
case "reach_a_percentage_of_destination":
return getElapsedView(request.params.arguments?.timeElapsedPercentage);
case "get_traveler_location":
return getCurrentLocationInfo(false, true);
case "set_current_location":
case "set_traveler_location":
return setCurrentLocation(String(request.params.arguments?.address));
case "get_destination_address":
case "get_traveler_destination_address":
return getDestinationAddress();
case "set_destination_address":
case "set_traveler_destination_address":
return setDestinationAddress(String(request.params.arguments?.address));
case "start_journey":
case "start_traveler_journey":
return startJourney();
case "stop_journey":
case "stop_traveler_journey":
return stopJourney();
case "call_traveler":
return setTravelerExist(true);
case "kick_traveler":
return setTravelerExist(false);
case "get_sns_mentions":
return getSnsMentions();
case "read_sns_reader":
return readSnsReader();
case "get_sns_feeds":
return getSnsFeeds();
case "post_sns_writer":
return postSnsWriter(String(request.params.arguments?.message));
case "reply_sns_writer":
return replySnsWriter(String(request.params.arguments?.message), String(request.params.arguments?.id));
case "add_like":
return addLike(String(request.params.arguments?.id));
default:
return Effect.fail(new Error(`Unknown tool:${request.params.name}`));
}
};
const run = () => {
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: "file:///role.txt",
mimeType: "text/plain",
name: "role.txt",
description: "The purpose and role of AI"
}, {
uri: "file:///roleWithSns.txt",
mimeType: "text/plain",
name: "roleWithSns.txt",
description: "The purpose and role of AI with SNS"
}, {
uri: "file:///carBattle.txt",
mimeType: "text/plain",
name: "carBattle.txt",
description: "Play the fantasy role playing"
}, {
uri: "file:///credit.txt",
mimeType: "text/plain",
name: "credit.txt",
description: "credit of this component"
}, {
uri: "file:///setting.txt",
mimeType: "text/plain",
name: "setting.txt",
description: "setting of traveler"
}
]
};
});
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const url = new URL(request.params.uri);
return await StoryService.getSettingResource(url.pathname).pipe(Effect.andThen(a => ({
contents: [{
uri: request.params.uri,
mimeType: "text/plain",
text: a
}]
})), Effect.provide([MapServiceLive, DbServiceLive, StoryServiceLive, RunnerServiceLive, FetchHttpClient.layer, ImageServiceLive]), Effect.runPromise).catch(e => {
if (e instanceof Error) {
throw new Error(e.message);
}
throw e;
});
});
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [
{
name: "tips",
description: "Inform you of recommended actions for traveler",
}
]
};
});
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
if (request.params.name !== "tips") {
throw new Error("Unknown prompt");
}
return {
messages: [
{
role: "user",
content: {
type: "text",
text: "Get tips information."
}
}
]
};
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return await makeToolsDef().pipe(Effect.runPromise);
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
return await McpLogService.logTrace(request.params.name).pipe(Effect.andThen(() => {
logSync('request.params:', JSON.stringify(request.params));
env.progressToken = request.params._meta?.progressToken;
return toolSwitch(request);
}), Effect.andThen(a => noImageOut ? a.filter(v => v.type !== 'image') : a), Effect.provide([DbServiceLive, ImageServiceLive]), Effect.andThen(a => ({ content: a })), Effect.catchIf(a => a instanceof AnswerError, e => {
return Effect.succeed({
content: [{
type: "text",
text: e.message
}]
});
}), Effect.catchAll(e => {
return McpLogService.logError(`catch all:${e.toString()},${JSON.stringify(e)}`).pipe(Effect.as({
isError: true,
content: [{
type: "text",
text: "Sorry,unknown system error."
}]
}), Effect.provide(McpLogServiceLive));
}), Effect.runPromise);
});
const transport = new StdioServerTransport();
const p = Effect.gen(function* () {
yield* Effect.forkDaemon(DbService.initSystemMode());
yield* Effect.tryPromise({
try: () => {
return server.connect(transport);
},
catch: error => {
return new Error(`mcp server error:${error}`);
}
});
}).pipe(Effect.provide(DbServiceLive));
return Effect.runFork(p);
};
return {
tips,
run,
sendLoggingMessage,
setPersonMode,
getTravelerInfo,
setTravelerInfo,
getCurrentLocationInfo,
setCurrentLocation,
getDestinationAddress,
setDestinationAddress,
startJourney,
stopJourney,
getSnsFeeds,
getSnsMentions,
replySnsWriter,
toolSwitch,
getSetting,
makeToolsDef,
};
})
}) {
}
export const McpServiceLive = McpService.Default;