index.jsโข20.3 kB
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import os from 'os';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// ํ๋ฅด์๋ ์ ์ฅ ๋๋ ํ ๋ฆฌ
const PERSONA_DIR = path.join(os.homedir(), '.persona');
const ANALYTICS_FILE = path.join(PERSONA_DIR, '.analytics.json');
const COMMUNITY_DIR = path.join(__dirname, 'community');
// ํ๋ฅด์๋ ๋๋ ํ ๋ฆฌ ์ด๊ธฐํ
async function initPersonaDir() {
try {
await fs.mkdir(PERSONA_DIR, { recursive: true });
} catch (error) {
console.error('Failed to create persona directory:', error);
}
}
// ํ๋ฅด์๋ ํ์ผ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
async function listPersonas() {
try {
const files = await fs.readdir(PERSONA_DIR);
return files.filter(f => f.endsWith('.txt')).map(f => f.replace('.txt', ''));
} catch (error) {
return [];
}
}
// ํ๋ฅด์๋ ์ฝ๊ธฐ
async function readPersona(name) {
const filePath = path.join(PERSONA_DIR, `${name}.txt`);
try {
const content = await fs.readFile(filePath, 'utf-8');
return content;
} catch (error) {
throw new Error(`Persona "${name}" not found`);
}
}
// ํ๋ฅด์๋ ์ ์ฅ
async function savePersona(name, content) {
const filePath = path.join(PERSONA_DIR, `${name}.txt`);
await fs.writeFile(filePath, content, 'utf-8');
}
// ํ๋ฅด์๋ ์ญ์
async function deletePersona(name) {
const filePath = path.join(PERSONA_DIR, `${name}.txt`);
await fs.unlink(filePath);
}
// ๋ถ์ ๋ฐ์ดํฐ ๋ก๋
async function loadAnalytics() {
try {
const data = await fs.readFile(ANALYTICS_FILE, 'utf-8');
return JSON.parse(data);
} catch {
return { usage: {}, contextPatterns: {} };
}
}
// ๋ถ์ ๋ฐ์ดํฐ ์ ์ฅ
async function saveAnalytics(data) {
await fs.writeFile(ANALYTICS_FILE, JSON.stringify(data, null, 2), 'utf-8');
}
// ์ฌ์ฉ ๊ธฐ๋ก ์ถ๊ฐ
async function trackUsage(personaName, context = '') {
const analytics = await loadAnalytics();
// ์ฌ์ฉ ํ์ ์ฆ๊ฐ
if (!analytics.usage[personaName]) {
analytics.usage[personaName] = 0;
}
analytics.usage[personaName]++;
// ์ปจํ
์คํธ ํจํด ์ ์ฅ (๊ฒฝ๋ํ: ํค์๋๋ง)
if (context) {
const keywords = context.toLowerCase().match(/\b\w{4,}\b/g) || [];
if (!analytics.contextPatterns[personaName]) {
analytics.contextPatterns[personaName] = {};
}
keywords.slice(0, 5).forEach(kw => {
analytics.contextPatterns[personaName][kw] =
(analytics.contextPatterns[personaName][kw] || 0) + 1;
});
}
await saveAnalytics(analytics);
}
// ์ปค๋ฎค๋ํฐ ํ๋ฅด์๋ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
async function listCommunityPersonas() {
try {
const files = await fs.readdir(COMMUNITY_DIR);
const personas = [];
for (const file of files.filter(f => f.endsWith('.txt'))) {
const name = file.replace('.txt', '');
const filePath = path.join(COMMUNITY_DIR, file);
const content = await fs.readFile(filePath, 'utf-8');
// ๋ฉํ๋ฐ์ดํฐ ์ถ์ถ
const lines = content.split('\n');
const metadata = {};
for (const line of lines) {
if (line.startsWith('# ')) {
const match = line.match(/^# (\w+):\s*(.+)$/);
if (match) {
metadata[match[1].toLowerCase()] = match[2];
}
} else if (!line.startsWith('#')) {
break; // ๋ฉํ๋ฐ์ดํฐ ์น์
๋
}
}
personas.push({
name,
...metadata,
file
});
}
return personas;
} catch (error) {
console.error('Failed to list community personas:', error);
return [];
}
}
// ์ปค๋ฎค๋ํฐ ํ๋ฅด์๋ ์ฝ๊ธฐ
async function readCommunityPersona(name) {
const filePath = path.join(COMMUNITY_DIR, `${name}.txt`);
try {
const content = await fs.readFile(filePath, 'utf-8');
return content;
} catch (error) {
throw new Error(`Community persona "${name}" not found`);
}
}
// ์ปค๋ฎค๋ํฐ ํ๋ฅด์๋๋ฅผ ๋ก์ปฌ์ ์ค์น
async function installCommunityPersona(name) {
const communityPath = path.join(COMMUNITY_DIR, `${name}.txt`);
const localPath = path.join(PERSONA_DIR, `${name}.txt`);
try {
const content = await fs.readFile(communityPath, 'utf-8');
await fs.writeFile(localPath, content, 'utf-8');
return localPath;
} catch (error) {
throw new Error(`Failed to install community persona "${name}": ${error.message}`);
}
}
// ์ค๋งํธ ํ๋ฅด์๋ ์ ์
async function suggestPersona(context) {
const personas = await listPersonas();
if (personas.length === 0) {
return null;
}
const analytics = await loadAnalytics();
const contextLower = context.toLowerCase();
// ์ปจํ
์คํธ ํค์๋ ๋ถ์
const detectionRules = [
{ keywords: ['explain', 'teach', 'learn', 'understand', 'how', 'what', 'why'], persona: 'teacher', weight: 3 },
{ keywords: ['code', 'function', 'bug', 'debug', 'program', 'implement'], persona: 'coder', weight: 3 },
{ keywords: ['professional', 'business', 'formal', 'report', 'meeting'], persona: 'professional', weight: 2 },
{ keywords: ['casual', 'chat', 'friendly', 'hey', 'talk'], persona: 'casual', weight: 2 },
{ keywords: ['brief', 'short', 'quick', 'summary', 'concise'], persona: 'concise', weight: 2 },
];
const scores = {};
// ๊ท์น ๊ธฐ๋ฐ ์ ์
detectionRules.forEach(rule => {
if (personas.includes(rule.persona)) {
const matchCount = rule.keywords.filter(kw => contextLower.includes(kw)).length;
if (matchCount > 0) {
scores[rule.persona] = (scores[rule.persona] || 0) + matchCount * rule.weight;
}
}
});
// ๊ณผ๊ฑฐ ์ฌ์ฉ ํจํด ๊ธฐ๋ฐ ์ ์ (๊ฐ์ค์น ๋ฎ๊ฒ)
const contextKeywords = contextLower.match(/\b\w{4,}\b/g) || [];
personas.forEach(persona => {
if (analytics.contextPatterns[persona]) {
contextKeywords.forEach(kw => {
if (analytics.contextPatterns[persona][kw]) {
scores[persona] = (scores[persona] || 0) + 0.5;
}
});
}
});
// ์ต๊ณ ์ ์ ํ๋ฅด์๋ ๋ฐํ
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
if (sorted.length > 0 && sorted[0][1] > 1) {
return {
persona: sorted[0][0],
confidence: Math.min(sorted[0][1] / 10, 0.95),
reason: `Context matches ${sorted[0][0]} pattern`,
};
}
return null;
}
// MCP ์๋ฒ ์์ฑ
const server = new Server(
{
name: 'persona-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
// ๋๊ตฌ ๋ชฉ๋ก
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'create_persona',
description: '์๋ก์ด ํ๋ฅด์๋ ํ๋กํ์ ์์ฑํฉ๋๋ค',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'ํ๋ฅด์๋ ์ด๋ฆ (์: default, professional, casual)',
},
content: {
type: 'string',
description: 'ํ๋ฅด์๋ ํ๋กฌํํธ ๋ด์ฉ',
},
},
required: ['name', 'content'],
},
},
{
name: 'update_persona',
description: '๊ธฐ์กด ํ๋ฅด์๋ ํ๋กํ์ ์์ ํฉ๋๋ค',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: '์์ ํ ํ๋ฅด์๋ ์ด๋ฆ',
},
content: {
type: 'string',
description: '์๋ก์ด ํ๋ฅด์๋ ํ๋กฌํํธ ๋ด์ฉ',
},
},
required: ['name', 'content'],
},
},
{
name: 'delete_persona',
description: 'ํ๋ฅด์๋ ํ๋กํ์ ์ญ์ ํฉ๋๋ค',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: '์ญ์ ํ ํ๋ฅด์๋ ์ด๋ฆ',
},
},
required: ['name'],
},
},
{
name: 'list_personas',
description: '์ฌ์ฉ ๊ฐ๋ฅํ ๋ชจ๋ ํ๋ฅด์๋ ๋ชฉ๋ก์ ์กฐํํฉ๋๋ค',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'suggest_persona',
description: '๋ํ ์ปจํ
์คํธ๋ฅผ ๋ถ์ํ์ฌ ์ ํฉํ ํ๋ฅด์๋๋ฅผ ์ ์ํฉ๋๋ค (ํธ๋ฆฌ๊ฑฐ ์์๋ง ํ์ฑํ)',
inputSchema: {
type: 'object',
properties: {
context: {
type: 'string',
description: '๋ถ์ํ ๋ํ ์ปจํ
์คํธ ๋๋ ์ง๋ฌธ ๋ด์ฉ',
},
},
required: ['context'],
},
},
{
name: 'chain_personas',
description: '์ฌ๋ฌ ํ๋ฅด์๋๋ฅผ ์์ฐจ์ ์ผ๋ก ์คํํ์ฌ ๋จ๊ณ๋ณ ์ฒ๋ฆฌ๋ฅผ ์ํํฉ๋๋ค',
inputSchema: {
type: 'object',
properties: {
personas: {
type: 'array',
items: { type: 'string' },
description: '์์ฐจ ์คํํ ํ๋ฅด์๋ ์ด๋ฆ ๋ฐฐ์ด',
},
initialInput: {
type: 'string',
description: '์ฒซ ๋ฒ์งธ ํ๋ฅด์๋์ ์ ๋ฌํ ์
๋ ฅ',
},
},
required: ['personas', 'initialInput'],
},
},
{
name: 'get_analytics',
description: 'ํ๋ฅด์๋ ์ฌ์ฉ ํต๊ณ๋ฅผ ์กฐํํฉ๋๋ค (๋ก์ปฌ ๋ฐ์ดํฐ๋ง)',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'browse_community',
description: '์ปค๋ฎค๋ํฐ ํ๋ฅด์๋ ์ปฌ๋ ์
์ ํ์ํฉ๋๋ค (GitHub์์ ๊ณต์ ๋ ๋ฌด๋ฃ ํ๋ฅด์๋)',
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'ํํฐ๋งํ ์นดํ
๊ณ ๋ฆฌ (์ ํ์ฌํญ): Programming, Creative, Business, Education, Design ๋ฑ',
},
},
},
},
{
name: 'install_community_persona',
description: '์ปค๋ฎค๋ํฐ ํ๋ฅด์๋๋ฅผ ๋ก์ปฌ ์ปฌ๋ ์
์ ์ค์นํฉ๋๋ค',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: '์ค์นํ ์ปค๋ฎค๋ํฐ ํ๋ฅด์๋ ์ด๋ฆ',
},
},
required: ['name'],
},
},
],
};
});
// ๋๊ตฌ ์คํ
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'create_persona': {
await savePersona(args.name, args.content);
return {
content: [
{
type: 'text',
text: `ํ๋ฅด์๋ "${args.name}"์ด(๊ฐ) ์์ฑ๋์์ต๋๋ค.\n์์น: ${path.join(PERSONA_DIR, args.name + '.txt')}`,
},
],
};
}
case 'update_persona': {
await savePersona(args.name, args.content);
return {
content: [
{
type: 'text',
text: `ํ๋ฅด์๋ "${args.name}"์ด(๊ฐ) ์
๋ฐ์ดํธ๋์์ต๋๋ค.`,
},
],
};
}
case 'delete_persona': {
await deletePersona(args.name);
return {
content: [
{
type: 'text',
text: `ํ๋ฅด์๋ "${args.name}"์ด(๊ฐ) ์ญ์ ๋์์ต๋๋ค.`,
},
],
};
}
case 'list_personas': {
const personas = await listPersonas();
return {
content: [
{
type: 'text',
text: personas.length > 0
? `์ฌ์ฉ ๊ฐ๋ฅํ ํ๋ฅด์๋:\n${personas.map(p => `- ${p}`).join('\n')}\n\n์ฌ์ฉ๋ฒ: @persona:${personas[0]} ํ์์ผ๋ก ์ฐธ์กฐํ์ธ์.`
: '์ ์ฅ๋ ํ๋ฅด์๋๊ฐ ์์ต๋๋ค.',
},
],
};
}
case 'suggest_persona': {
const suggestion = await suggestPersona(args.context);
if (!suggestion) {
return {
content: [
{
type: 'text',
text: '๐ก ํ์ฌ ์ปจํ
์คํธ์ ์ ํฉํ ํ๋ฅด์๋๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.\n์ฌ์ฉ ๊ฐ๋ฅํ ํ๋ฅด์๋ ๋ชฉ๋ก์ ๋ณด๋ ค๋ฉด list_personas ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ธ์.',
},
],
};
}
return {
content: [
{
type: 'text',
text: `๐ก ํ๋ฅด์๋ ์ ์\n\n์ถ์ฒ: @persona:${suggestion.persona}\n์ ๋ขฐ๋: ${(suggestion.confidence * 100).toFixed(0)}%\n์ด์ : ${suggestion.reason}\n\n์ด ํ๋ฅด์๋๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด @persona:${suggestion.persona} ๋ฆฌ์์ค๋ฅผ ์ฐธ์กฐํ์ธ์.`,
},
],
};
}
case 'chain_personas': {
const results = [];
let currentInput = args.initialInput;
for (const personaName of args.personas) {
try {
const personaContent = await readPersona(personaName);
await trackUsage(personaName, currentInput);
results.push({
persona: personaName,
prompt: personaContent,
input: currentInput,
});
// ๋ค์ ์
๋ ฅ์ ํ์ฌ ํ๋ฅด์๋์ ์ถ๋ ฅ์ด ๋ ๊ฒ์์ ๋ช
์
currentInput = `[Previous output from ${personaName} will be used as input here]`;
} catch (error) {
results.push({
persona: personaName,
error: error.message,
});
break;
}
}
const resultText = results.map((r, i) => {
if (r.error) {
return `Step ${i + 1} - ${r.persona}: โ ${r.error}`;
}
return `Step ${i + 1} - ${r.persona}:\n\nPrompt:\n${r.prompt}\n\nInput:\n${r.input}\n`;
}).join('\n' + '='.repeat(50) + '\n\n');
return {
content: [
{
type: 'text',
text: `๐ Persona Chain Execution\n\n${resultText}\nโ
Chain completed: ${results.filter(r => !r.error).length}/${args.personas.length} steps`,
},
],
};
}
case 'get_analytics': {
const analytics = await loadAnalytics();
const usageList = Object.entries(analytics.usage)
.sort((a, b) => b[1] - a[1])
.map(([name, count]) => ` ${name}: ${count} uses`)
.join('\n');
const topPatterns = {};
Object.entries(analytics.contextPatterns).forEach(([persona, patterns]) => {
const sorted = Object.entries(patterns)
.sort((a, b) => b[1] - a[1])
.slice(0, 3);
topPatterns[persona] = sorted.map(([kw]) => kw);
});
const patternsList = Object.entries(topPatterns)
.map(([persona, keywords]) => ` ${persona}: ${keywords.join(', ')}`)
.join('\n');
return {
content: [
{
type: 'text',
text: `๐ Persona Usage Analytics\n\n์ฌ์ฉ ํ์:\n${usageList || ' (no data)'}\n\n์ฃผ์ ์ปจํ
์คํธ ํจํด:\n${patternsList || ' (no data)'}\n\n๐ก ์ด ๋ฐ์ดํฐ๋ ๋ก์ปฌ์๋ง ์ ์ฅ๋๋ฉฐ ์ ์ก๋์ง ์์ต๋๋ค.`,
},
],
};
}
case 'browse_community': {
const personas = await listCommunityPersonas();
if (personas.length === 0) {
return {
content: [
{
type: 'text',
text: '๐ฆ ์ปค๋ฎค๋ํฐ ํ๋ฅด์๋๊ฐ ์์ง ์์ต๋๋ค.\n\nCONTRIBUTING.md๋ฅผ ์ฐธ์กฐํ์ฌ ์ฒซ ๋ฒ์งธ ๊ธฐ์ฌ์๊ฐ ๋์ด๋ณด์ธ์!',
},
],
};
}
// ์นดํ
๊ณ ๋ฆฌ ํํฐ๋ง
let filtered = personas;
if (args.category) {
filtered = personas.filter(p =>
p.category && p.category.toLowerCase().includes(args.category.toLowerCase())
);
}
// ์นดํ
๊ณ ๋ฆฌ๋ณ๋ก ๊ทธ๋ฃนํ
const byCategory = {};
filtered.forEach(p => {
const cat = p.category || 'Other';
if (!byCategory[cat]) {
byCategory[cat] = [];
}
byCategory[cat].push(p);
});
let output = '๐ Community Persona Collection\n\n';
output += `Found ${filtered.length} persona(s)${args.category ? ` in category "${args.category}"` : ''}\n\n`;
for (const [category, list] of Object.entries(byCategory)) {
output += `## ${category}\n\n`;
list.forEach(p => {
output += `### ${p.name}\n`;
if (p.author) output += `๐ค Author: ${p.author}\n`;
if (p.difficulty) output += `๐ Difficulty: ${p.difficulty}\n`;
if (p.persona) output += `๐ Description: ${p.persona}\n`;
if (p['use']) output += `๐ก Use Cases: ${p['use']}\n`;
output += `\n๐ฅ Install: \`install_community_persona\` with name "${p.name}"\n\n`;
});
}
output += '\n---\n\n';
output += '๐ก **Tip**: After installing, use @persona:name to activate\n';
output += '๐ **More info**: See CONTRIBUTING.md to add your own persona\n';
output += '๐ฏ **Vision**: Check VISION.md for the Persona Marketplace roadmap';
return {
content: [
{
type: 'text',
text: output,
},
],
};
}
case 'install_community_persona': {
const installedPath = await installCommunityPersona(args.name);
// ๊ฐ๋จํ ํ๋ฆฌ๋ทฐ ์ ๊ณต
const content = await readCommunityPersona(args.name);
const preview = content.split('\n').slice(0, 10).join('\n');
return {
content: [
{
type: 'text',
text: `โ
Persona "${args.name}" installed successfully!\n\n๐ Location: ${installedPath}\n\n๐ Preview:\n${preview}\n...\n\n๐ก **How to use:**\n@persona:${args.name} your question or task\n\nExample:\n@persona:${args.name} help me with this code\n\n๐ฏ The persona will only activate when you use the @persona:${args.name} trigger (Submarine Mode = 0 tokens otherwise)`,
},
],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
return {
content: [
{
type: 'text',
text: `์ค๋ฅ: ${error.message}`,
},
],
isError: true,
};
}
});
// ๋ฆฌ์์ค ๋ชฉ๋ก
server.setRequestHandler(ListResourcesRequestSchema, async () => {
const personas = await listPersonas();
return {
resources: personas.map(name => ({
uri: `persona://${name}`,
mimeType: 'text/plain',
name: `Persona: ${name}`,
description: `${name} ํ๋ฅด์๋ ํ๋กํ`,
})),
};
});
// ๋ฆฌ์์ค ์ฝ๊ธฐ
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const match = uri.match(/^persona:\/\/(.+)$/);
if (!match) {
throw new Error('Invalid persona URI');
}
const personaName = match[1];
const content = await readPersona(personaName);
// ์ฌ์ฉ ์ถ์ (ํธ๋ฆฌ๊ฑฐ ๊ธฐ๋ฐ - ์ค์ ๋ก๋ ์์๋ง)
await trackUsage(personaName, '');
return {
contents: [
{
uri,
mimeType: 'text/plain',
text: content,
},
],
};
});
// ์๋ฒ ์์
async function main() {
await initPersonaDir();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Persona MCP server running on stdio');
}
main().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});