slima-mcp 0.1.15 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -74
- package/dist/index.js +246 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -95,16 +95,34 @@ var SlimaApiClient = class {
|
|
|
95
95
|
headers: {
|
|
96
96
|
Authorization: `Bearer ${token}`,
|
|
97
97
|
"Content-Type": "application/json",
|
|
98
|
-
"User-Agent":
|
|
98
|
+
"User-Agent": `slima-mcp-server/${"0.2.0"}`
|
|
99
99
|
},
|
|
100
100
|
body: body ? JSON.stringify(body) : void 0
|
|
101
101
|
});
|
|
102
|
+
this.handleUpgradeHint(response);
|
|
102
103
|
if (!response.ok) {
|
|
103
104
|
await this.handleErrorResponse(response);
|
|
104
105
|
}
|
|
105
106
|
const json = await response.json();
|
|
106
107
|
return json.data;
|
|
107
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Surface the X-Slima-MCP-Upgrade-Hint header (if present) as a warn log.
|
|
111
|
+
* Deduped per (hint, baseUrl) to keep stderr quiet during long sessions.
|
|
112
|
+
* Defensive: mocked Response objects in tests may not carry a full Headers
|
|
113
|
+
* interface, so guard the access rather than hard-crashing the request.
|
|
114
|
+
*/
|
|
115
|
+
loggedUpgradeHints = /* @__PURE__ */ new Set();
|
|
116
|
+
handleUpgradeHint(response) {
|
|
117
|
+
const hint = response?.headers?.get?.("X-Slima-MCP-Upgrade-Hint");
|
|
118
|
+
if (!hint) return;
|
|
119
|
+
const key = `${hint}::${this.baseUrl}`;
|
|
120
|
+
if (this.loggedUpgradeHints.has(key)) return;
|
|
121
|
+
this.loggedUpgradeHints.add(key);
|
|
122
|
+
this.logger.warn(
|
|
123
|
+
`[slima-mcp] ${hint} \u2014 upgrade options: npm install -g slima-mcp@latest, or change your Claude Desktop config to "npx -y slima-mcp@0".`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
108
126
|
/**
|
|
109
127
|
* Handle error response
|
|
110
128
|
*/
|
|
@@ -409,10 +427,30 @@ function formatFileTree(files, indent = 0) {
|
|
|
409
427
|
}
|
|
410
428
|
|
|
411
429
|
// src/core/tools/books.ts
|
|
430
|
+
function studioLabel(book) {
|
|
431
|
+
return book.bookType === "script" ? "\u{1F4DD} Script Studio" : "\u{1F4D6} Writing Studio";
|
|
432
|
+
}
|
|
433
|
+
function scriptStudioWriteRulesBlock(book) {
|
|
434
|
+
return `### \u{1F512} Write Restrictions (Script Studio)
|
|
435
|
+
|
|
436
|
+
This book uses **structured schemas**. Via MCP you can:
|
|
437
|
+
- \u2705 **Read** any file (series.json, *.character, *.scene, etc.)
|
|
438
|
+
- \u2705 **Write / Edit / Delete** under \`.script_studio/planning/**/*\` only (free-form reference tree)
|
|
439
|
+
- \u274C **Cannot modify** structured files (\`series.json\`, \`season.json\`, \`episode.json\`, \`*.character\`, \`*.location\`, \`*.scene\`, \`*.storyline\`, \`*.note\`)
|
|
440
|
+
- \u274C **Cannot modify** \`.script_studio/planning/.initialized\` (frontend bootstrap sentinel)
|
|
441
|
+
|
|
442
|
+
To add characters / scenes / storylines, use the Script Studio UI in the Slima app.
|
|
443
|
+
|
|
444
|
+
For full per-book details, read the MCP resource: \`slima://books/${book.token}/schema\`.`;
|
|
445
|
+
}
|
|
412
446
|
function registerBookTools(server, client) {
|
|
413
447
|
server.tool(
|
|
414
448
|
"create_book",
|
|
415
|
-
|
|
449
|
+
`Create a new **Writing Studio** book in your Slima library.
|
|
450
|
+
|
|
451
|
+
Only Writing Studio (free-form novels / long-form prose) books can be created via MCP.
|
|
452
|
+
To create a **Script Studio** book (screenwriting with structured series/seasons/episodes/scenes),
|
|
453
|
+
please use the Slima app UI \u2014 MCP does not yet support Script Studio scaffolding.`,
|
|
416
454
|
{
|
|
417
455
|
title: z.string().describe("Book title (required)"),
|
|
418
456
|
author_name: z.string().optional().describe("Author name (optional)"),
|
|
@@ -468,7 +506,7 @@ You can now add files using create_file with book_token: ${book.token}`
|
|
|
468
506
|
}
|
|
469
507
|
const formatted = books.map((book) => {
|
|
470
508
|
const words = book.totalWordCount?.toLocaleString() || "0";
|
|
471
|
-
return `- **${book.title}** (${book.token})
|
|
509
|
+
return `- **${book.title}** (${book.token}) \u2014 ${studioLabel(book)}
|
|
472
510
|
Author: ${book.authorName || "N/A"} | Words: ${words}`;
|
|
473
511
|
}).join("\n\n");
|
|
474
512
|
return {
|
|
@@ -477,7 +515,9 @@ You can now add files using create_file with book_token: ${book.token}`
|
|
|
477
515
|
type: "text",
|
|
478
516
|
text: `Found ${books.length} book(s):
|
|
479
517
|
|
|
480
|
-
${formatted}
|
|
518
|
+
${formatted}
|
|
519
|
+
|
|
520
|
+
_\u{1F4DD} Script Studio books have write restrictions via MCP (see \`get_book\` or resource \`slima://books/{token}/schema\`). \u{1F4D6} Writing Studio books are fully writable._`
|
|
481
521
|
}
|
|
482
522
|
]
|
|
483
523
|
};
|
|
@@ -499,7 +539,7 @@ ${formatted}`
|
|
|
499
539
|
async ({ book_token }) => {
|
|
500
540
|
try {
|
|
501
541
|
const book = await client.getBook(book_token);
|
|
502
|
-
const
|
|
542
|
+
const header = `# ${studioLabel(book)} \u2014 ${book.title}
|
|
503
543
|
|
|
504
544
|
- **Token**: ${book.token}
|
|
505
545
|
- **Author**: ${book.authorName || "N/A"}
|
|
@@ -510,6 +550,9 @@ ${formatted}`
|
|
|
510
550
|
- **Reference Words**: ${book.referenceWordCount?.toLocaleString() || "0"}
|
|
511
551
|
- **Created**: ${book.createdAt}
|
|
512
552
|
- **Updated**: ${book.updatedAt}`;
|
|
553
|
+
const text = book.bookType === "script" ? `${header}
|
|
554
|
+
|
|
555
|
+
${scriptStudioWriteRulesBlock(book)}` : header;
|
|
513
556
|
return {
|
|
514
557
|
content: [{ type: "text", text }]
|
|
515
558
|
};
|
|
@@ -530,7 +573,10 @@ ${formatted}`
|
|
|
530
573
|
{ readOnlyHint: true, openWorldHint: true },
|
|
531
574
|
async ({ book_token }) => {
|
|
532
575
|
try {
|
|
533
|
-
const commits = await
|
|
576
|
+
const [book, commits] = await Promise.all([
|
|
577
|
+
client.getBook(book_token),
|
|
578
|
+
client.listCommits(book_token, 1)
|
|
579
|
+
]);
|
|
534
580
|
if (commits.length === 0) {
|
|
535
581
|
return {
|
|
536
582
|
content: [
|
|
@@ -544,13 +590,16 @@ ${formatted}`
|
|
|
544
590
|
const flatSnapshot = commits[0].filesSnapshot;
|
|
545
591
|
const nestedTree = buildFileTree(flatSnapshot);
|
|
546
592
|
const tree = formatFileTree(nestedTree);
|
|
593
|
+
const footer = book.bookType === "script" ? `
|
|
594
|
+
|
|
595
|
+
_\u{1F4DD} This is a Script Studio book. Via MCP, writes are restricted to \`.script_studio/planning/**/*\`. For the full write-restriction spec, read resource \`slima://books/${book.token}/schema\`._` : "";
|
|
547
596
|
return {
|
|
548
597
|
content: [
|
|
549
598
|
{
|
|
550
599
|
type: "text",
|
|
551
|
-
text: `File structure for book ${book_token}:
|
|
600
|
+
text: `File structure for ${studioLabel(book)} \u2014 ${book.title} (${book_token}):
|
|
552
601
|
|
|
553
|
-
${tree}`
|
|
602
|
+
${tree}${footer}`
|
|
554
603
|
}
|
|
555
604
|
]
|
|
556
605
|
};
|
|
@@ -826,15 +875,31 @@ ${formatted}
|
|
|
826
875
|
);
|
|
827
876
|
server.tool(
|
|
828
877
|
"analyze_chapter",
|
|
829
|
-
|
|
878
|
+
`Get AI Beta Reader feedback on a chapter from a specific reader persona. This may take a moment to complete.
|
|
879
|
+
|
|
880
|
+
**Not supported for Script Studio books** (\`book_type: "script"\`) \u2014 AI Beta Reader is a Writing Studio feature and does not yet parse \`.scene\` or \`series.json\` structures. Use the Script Studio UI's built-in analysis for screenplays.`,
|
|
830
881
|
{
|
|
831
|
-
book_token: z3.string().describe(
|
|
882
|
+
book_token: z3.string().describe('Book token (e.g., bk_abc123) \u2014 must be a Writing Studio book (book_type: "book").'),
|
|
832
883
|
file_path: z3.string().describe("Chapter file path (e.g., /chapters/01.md)"),
|
|
833
884
|
persona_token: z3.string().describe("Persona token (e.g., psn_xxx). Use list_personas to find available personas.")
|
|
834
885
|
},
|
|
835
886
|
{ readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
836
887
|
async ({ book_token, file_path, persona_token }) => {
|
|
837
888
|
try {
|
|
889
|
+
const book = await client.getBook(book_token);
|
|
890
|
+
if (book.bookType === "script") {
|
|
891
|
+
return {
|
|
892
|
+
content: [
|
|
893
|
+
{
|
|
894
|
+
type: "text",
|
|
895
|
+
text: `\u274C analyze_chapter is not available for Script Studio books.
|
|
896
|
+
|
|
897
|
+
Book "${book.title}" (${book.token}) is a Script Studio book. Please use the Script Studio UI's built-in analysis features (structural notes, character consistency, scene-level feedback) \u2014 AI Beta Reader is a Writing Studio feature.`
|
|
898
|
+
}
|
|
899
|
+
],
|
|
900
|
+
isError: true
|
|
901
|
+
};
|
|
902
|
+
}
|
|
838
903
|
const commits = await client.listCommits(book_token, 1);
|
|
839
904
|
if (commits.length === 0) {
|
|
840
905
|
return {
|
|
@@ -1179,6 +1244,18 @@ function searchByName2(files, name) {
|
|
|
1179
1244
|
|
|
1180
1245
|
// src/core/tools/files.ts
|
|
1181
1246
|
import { z as z4 } from "zod";
|
|
1247
|
+
var SCRIPT_STUDIO_WRITE_WARNING = `
|
|
1248
|
+
**IMPORTANT \u2014 Script Studio books (\`book_type: "script"\`):**
|
|
1249
|
+
Writes are restricted to \`.script_studio/planning/**/*\` only.
|
|
1250
|
+
Structured files (\`series.json\`, \`*.character\`, \`*.scene\`, \`*.storyline\`, \`*.note\`, \`*.location\`, \`season.json\`, \`episode.json\`) are READ-ONLY via MCP \u2014 structural edits must go through the Script Studio UI.
|
|
1251
|
+
The sentinel file \`.script_studio/planning/.initialized\` is also read-only (frontend bootstrap marker).
|
|
1252
|
+
Attempting to write outside \`planning/\` returns HTTP 400 with code \`INVALID_PATH\`.
|
|
1253
|
+
For per-book details, read resource \`slima://books/{book_token}/schema\`.
|
|
1254
|
+
|
|
1255
|
+
For Writing Studio books (\`book_type: "book"\`), all paths remain writable.
|
|
1256
|
+
Always check \`book_type\` via \`list_books\` or \`get_book\` before writing to an unfamiliar book.`;
|
|
1257
|
+
var PATH_PARAM_HINT = 'File path relative to book root. For Script Studio books: MUST be under ".script_studio/planning/" (e.g., ".script_studio/planning/references/outline.md"). For Writing Studio books: any path (e.g., "characters/protagonist.md").';
|
|
1258
|
+
var SCRIPT_STRUCTURED_EXTENSIONS = ["character", "scene", "storyline", "note", "location"];
|
|
1182
1259
|
function registerFileTools(server, client) {
|
|
1183
1260
|
server.tool(
|
|
1184
1261
|
"read_file",
|
|
@@ -1215,10 +1292,11 @@ function registerFileTools(server, client) {
|
|
|
1215
1292
|
);
|
|
1216
1293
|
server.tool(
|
|
1217
1294
|
"write_file",
|
|
1218
|
-
|
|
1295
|
+
`Replace the entire content of an existing file (creates a new commit).
|
|
1296
|
+
${SCRIPT_STUDIO_WRITE_WARNING}`,
|
|
1219
1297
|
{
|
|
1220
1298
|
book_token: z4.string().describe("Book token (e.g., bk_abc123)"),
|
|
1221
|
-
path: z4.string().describe(
|
|
1299
|
+
path: z4.string().describe(PATH_PARAM_HINT),
|
|
1222
1300
|
content: z4.string().describe("New content to write"),
|
|
1223
1301
|
commit_message: z4.string().optional().describe("Commit message (optional, auto-generated if not provided)")
|
|
1224
1302
|
},
|
|
@@ -1251,10 +1329,11 @@ Message: ${result.commit.name || result.commit.message}`
|
|
|
1251
1329
|
);
|
|
1252
1330
|
server.tool(
|
|
1253
1331
|
"edit_file",
|
|
1254
|
-
|
|
1332
|
+
`Edit a file by replacing a specific text segment (search and replace). Use this for precise edits instead of rewriting the entire file.
|
|
1333
|
+
${SCRIPT_STUDIO_WRITE_WARNING}`,
|
|
1255
1334
|
{
|
|
1256
1335
|
book_token: z4.string().describe("Book token (e.g., bk_abc123)"),
|
|
1257
|
-
path: z4.string().describe(
|
|
1336
|
+
path: z4.string().describe(PATH_PARAM_HINT),
|
|
1258
1337
|
old_string: z4.string().describe("The exact text to find and replace (must match exactly, including whitespace)"),
|
|
1259
1338
|
new_string: z4.string().describe("The new text to replace it with"),
|
|
1260
1339
|
replace_all: z4.boolean().optional().describe("Replace all occurrences (default: false, replaces only first match)"),
|
|
@@ -1326,10 +1405,13 @@ ${old_string.slice(0, 200)}${old_string.length > 200 ? "..." : ""}
|
|
|
1326
1405
|
);
|
|
1327
1406
|
server.tool(
|
|
1328
1407
|
"create_file",
|
|
1329
|
-
|
|
1408
|
+
`Create a new file in a book (creates a new commit).
|
|
1409
|
+
${SCRIPT_STUDIO_WRITE_WARNING}`,
|
|
1330
1410
|
{
|
|
1331
1411
|
book_token: z4.string().describe("Book token (e.g., bk_abc123)"),
|
|
1332
|
-
path: z4.string().describe(
|
|
1412
|
+
path: z4.string().describe(
|
|
1413
|
+
`${PATH_PARAM_HINT} Parent folders are auto-created (mkdir -p behavior).`
|
|
1414
|
+
),
|
|
1333
1415
|
content: z4.string().optional().describe("Initial content (optional, defaults to empty)"),
|
|
1334
1416
|
commit_message: z4.string().optional().describe("Commit message (optional, auto-generated if not provided)")
|
|
1335
1417
|
},
|
|
@@ -1363,10 +1445,11 @@ Message: ${result.commit.name || result.commit.message}`
|
|
|
1363
1445
|
);
|
|
1364
1446
|
server.tool(
|
|
1365
1447
|
"delete_file",
|
|
1366
|
-
|
|
1448
|
+
`Delete a file from a book (creates a new commit).
|
|
1449
|
+
${SCRIPT_STUDIO_WRITE_WARNING}`,
|
|
1367
1450
|
{
|
|
1368
1451
|
book_token: z4.string().describe("Book token (e.g., bk_abc123)"),
|
|
1369
|
-
path: z4.string().describe(
|
|
1452
|
+
path: z4.string().describe(PATH_PARAM_HINT),
|
|
1370
1453
|
commit_message: z4.string().optional().describe("Commit message (optional, auto-generated if not provided)")
|
|
1371
1454
|
},
|
|
1372
1455
|
{ readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
@@ -1397,10 +1480,11 @@ Message: ${result.commit.name || result.commit.message}`
|
|
|
1397
1480
|
);
|
|
1398
1481
|
server.tool(
|
|
1399
1482
|
"append_to_file",
|
|
1400
|
-
|
|
1483
|
+
`Append content to the end of an existing file (creates a new commit).
|
|
1484
|
+
${SCRIPT_STUDIO_WRITE_WARNING}`,
|
|
1401
1485
|
{
|
|
1402
1486
|
book_token: z4.string().describe("Book token (e.g., bk_abc123)"),
|
|
1403
|
-
path: z4.string().describe(
|
|
1487
|
+
path: z4.string().describe(PATH_PARAM_HINT),
|
|
1404
1488
|
content: z4.string().describe("Content to append"),
|
|
1405
1489
|
commit_message: z4.string().optional().describe("Commit message (optional, auto-generated if not provided)")
|
|
1406
1490
|
},
|
|
@@ -1433,27 +1517,45 @@ Message: ${result.commit.name || result.commit.message}`
|
|
|
1433
1517
|
);
|
|
1434
1518
|
server.tool(
|
|
1435
1519
|
"search_content",
|
|
1436
|
-
|
|
1520
|
+
`Search for content across all files in a book.
|
|
1521
|
+
|
|
1522
|
+
**Script Studio behavior:** By default, structured files (\`*.character\`, \`*.scene\`, \`*.storyline\`, \`*.note\`, \`*.location\`, and \`*.json\` under \`.script_studio/\`) are **excluded** from search results \u2014 their raw JSON is noisy and not useful for full-text search.
|
|
1523
|
+
Pass \`include_structured: true\` to search those files too.
|
|
1524
|
+
Writing Studio books are unaffected by this filter.`,
|
|
1437
1525
|
{
|
|
1438
1526
|
book_token: z4.string().describe("Book token (e.g., bk_abc123)"),
|
|
1439
1527
|
query: z4.string().describe('Search query (e.g., "blue eyes" or "protagonist name")'),
|
|
1440
1528
|
file_types: z4.array(z4.string()).optional().describe('Filter by file types (e.g., ["markdown", "txt"])'),
|
|
1441
|
-
limit: z4.number().optional().describe("Maximum number of matches to return (default: 20)")
|
|
1529
|
+
limit: z4.number().optional().describe("Maximum number of matches to return (default: 20)"),
|
|
1530
|
+
include_structured: z4.boolean().optional().describe(
|
|
1531
|
+
"Script Studio only: set to true to include structured files (*.character / *.scene / etc.) in search results. Default: false (excluded)."
|
|
1532
|
+
)
|
|
1442
1533
|
},
|
|
1443
1534
|
{ readOnlyHint: true, openWorldHint: true },
|
|
1444
|
-
async ({ book_token, query, file_types, limit }) => {
|
|
1535
|
+
async ({ book_token, query, file_types, limit, include_structured }) => {
|
|
1445
1536
|
try {
|
|
1537
|
+
let bookType;
|
|
1538
|
+
if (!include_structured) {
|
|
1539
|
+
try {
|
|
1540
|
+
const book = await client.getBook(book_token);
|
|
1541
|
+
bookType = book.bookType;
|
|
1542
|
+
} catch {
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1446
1545
|
const result = await client.searchFiles(book_token, {
|
|
1447
1546
|
query,
|
|
1448
1547
|
fileTypes: file_types,
|
|
1449
1548
|
limit
|
|
1450
1549
|
});
|
|
1451
|
-
|
|
1550
|
+
const shouldFilter = !include_structured && bookType === "script";
|
|
1551
|
+
const filteredMatches = shouldFilter ? result.matches.filter((m) => !isStructuredScriptFile(m.file.path)) : result.matches;
|
|
1552
|
+
if (filteredMatches.length === 0) {
|
|
1553
|
+
const suffix = shouldFilter && result.matches.length > 0 ? ` (${result.matches.length} match(es) in structured files were excluded \u2014 pass include_structured: true to see them)` : "";
|
|
1452
1554
|
return {
|
|
1453
1555
|
content: [
|
|
1454
1556
|
{
|
|
1455
1557
|
type: "text",
|
|
1456
|
-
text: `No matches found for "${query}".`
|
|
1558
|
+
text: `No matches found for "${query}"${suffix}.`
|
|
1457
1559
|
}
|
|
1458
1560
|
]
|
|
1459
1561
|
};
|
|
@@ -1461,10 +1563,17 @@ Message: ${result.commit.name || result.commit.message}`
|
|
|
1461
1563
|
const output = [
|
|
1462
1564
|
`# Search Results for "${query}"`,
|
|
1463
1565
|
"",
|
|
1464
|
-
`Found ${
|
|
1566
|
+
`Found ${filteredMatches.length} file(s) with matches:`,
|
|
1465
1567
|
""
|
|
1466
1568
|
];
|
|
1467
|
-
|
|
1569
|
+
if (shouldFilter && filteredMatches.length < result.matches.length) {
|
|
1570
|
+
const excluded = result.matches.length - filteredMatches.length;
|
|
1571
|
+
output.push(
|
|
1572
|
+
`_Note: excluded ${excluded} Script Studio structured file(s). Pass include_structured: true to include them._`
|
|
1573
|
+
);
|
|
1574
|
+
output.push("");
|
|
1575
|
+
}
|
|
1576
|
+
for (const match of filteredMatches) {
|
|
1468
1577
|
output.push(`## ${match.file.path}`);
|
|
1469
1578
|
output.push(`Words: ${match.file.wordCount} | Matches: ${match.matchCount}`);
|
|
1470
1579
|
output.push("");
|
|
@@ -1485,6 +1594,105 @@ Message: ${result.commit.name || result.commit.message}`
|
|
|
1485
1594
|
}
|
|
1486
1595
|
);
|
|
1487
1596
|
}
|
|
1597
|
+
function isStructuredScriptFile(path) {
|
|
1598
|
+
if (!path) return false;
|
|
1599
|
+
const lower = path.toLowerCase();
|
|
1600
|
+
if (lower.startsWith(".script_studio/") && lower.endsWith(".json")) return true;
|
|
1601
|
+
const ext = lower.split(".").pop();
|
|
1602
|
+
if (!ext) return false;
|
|
1603
|
+
return SCRIPT_STRUCTURED_EXTENSIONS.includes(ext);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// src/core/resources/book_schema.ts
|
|
1607
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1608
|
+
var BOOK_SCHEMA_URI_TEMPLATE = "slima://books/{book_token}/schema";
|
|
1609
|
+
function registerBookSchemaResource(server, client) {
|
|
1610
|
+
server.resource(
|
|
1611
|
+
"slima-book-schema",
|
|
1612
|
+
new ResourceTemplate(BOOK_SCHEMA_URI_TEMPLATE, {
|
|
1613
|
+
// Intentionally no `list` callback — books may number in the hundreds, so
|
|
1614
|
+
// we expose only the URI template via `resources/templates/list` and let
|
|
1615
|
+
// clients call `resources/read` with a concrete token.
|
|
1616
|
+
list: void 0
|
|
1617
|
+
}),
|
|
1618
|
+
{
|
|
1619
|
+
description: "Per-book MCP operation schema: which paths are readable/writable, which are structured (read-only), and the structured file catalog. Read this before planning writes on an unfamiliar book.",
|
|
1620
|
+
mimeType: "application/json"
|
|
1621
|
+
},
|
|
1622
|
+
async (uri, variables) => {
|
|
1623
|
+
const rawToken = variables["book_token"];
|
|
1624
|
+
const bookToken = Array.isArray(rawToken) ? rawToken[0] : rawToken;
|
|
1625
|
+
if (!bookToken) {
|
|
1626
|
+
throw new Error(
|
|
1627
|
+
`Invalid Slima book schema URI: missing book_token in ${uri.toString()}`
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
const book = await client.getBook(bookToken);
|
|
1631
|
+
const schema = buildBookSchema(book);
|
|
1632
|
+
return {
|
|
1633
|
+
contents: [
|
|
1634
|
+
{
|
|
1635
|
+
uri: uri.toString(),
|
|
1636
|
+
mimeType: "application/json",
|
|
1637
|
+
text: JSON.stringify(schema, null, 2)
|
|
1638
|
+
}
|
|
1639
|
+
]
|
|
1640
|
+
};
|
|
1641
|
+
}
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
function buildBookSchema(book) {
|
|
1645
|
+
const isScript = book.bookType === "script";
|
|
1646
|
+
return {
|
|
1647
|
+
book_token: book.token,
|
|
1648
|
+
book_title: book.title,
|
|
1649
|
+
book_type: book.bookType ?? "book",
|
|
1650
|
+
studio_label: isScript ? "Script Studio" : "Writing Studio",
|
|
1651
|
+
mcp_capabilities: {
|
|
1652
|
+
// Note: actual reads still go through validate_safe_path!
|
|
1653
|
+
// (blocks absolute paths, traversal, control chars) even though it's
|
|
1654
|
+
// not reflected in this allowlist.
|
|
1655
|
+
readable_paths: "Any path within the book tree (path-traversal and absolute paths still rejected).",
|
|
1656
|
+
writable_paths: isScript ? [".script_studio/planning/**/*"] : ["**/*"],
|
|
1657
|
+
writable_exceptions: isScript ? [".script_studio/planning/.initialized"] : [],
|
|
1658
|
+
readonly_paths: isScript ? [
|
|
1659
|
+
".script_studio/series.json",
|
|
1660
|
+
".script_studio/seasons/**/season.json",
|
|
1661
|
+
".script_studio/seasons/**/episodes/**/episode.json",
|
|
1662
|
+
".script_studio/seasons/**/scenes/*.scene",
|
|
1663
|
+
".script_studio/characters/**/*.character",
|
|
1664
|
+
".script_studio/locations/**/*.location",
|
|
1665
|
+
".script_studio/storylines/**/*.storyline",
|
|
1666
|
+
".script_studio/notes/**/*.note",
|
|
1667
|
+
".script_studio/planning/.initialized"
|
|
1668
|
+
] : [],
|
|
1669
|
+
reserved_sentinels: isScript ? [".script_studio/planning/.initialized"] : []
|
|
1670
|
+
},
|
|
1671
|
+
structured_file_types: isScript ? {
|
|
1672
|
+
"series.json": "Series metadata (title, genre, logline, theme, synopsis, acts)",
|
|
1673
|
+
"season.json": "Season metadata",
|
|
1674
|
+
"episode.json": "Episode metadata",
|
|
1675
|
+
"*.scene": "Scene content with scene-specific format",
|
|
1676
|
+
"*.character": "Character definition with scope (series / season / episode)",
|
|
1677
|
+
"*.location": "Location definition with scope",
|
|
1678
|
+
"*.storyline": "Storyline with tier and ID linkage",
|
|
1679
|
+
"*.note": "Note with scope"
|
|
1680
|
+
} : {},
|
|
1681
|
+
notes: isScript ? "Structured files require schema-aware edits \u2014 use the Script Studio UI. MCP write access is restricted to planning/ (free-form reference tree) to prevent schema corruption." : "Writing Studio books are free-form. No schema restrictions; any path is writable.",
|
|
1682
|
+
policy_source: "Rails: Mcp::FileOperationService#validate_studio_write_path!"
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// src/core/server-instructions.ts
|
|
1687
|
+
var SERVER_INSTRUCTIONS = `Slima is an AI writing IDE with two product lines, distinguished by the \`book_type\` field on every book:
|
|
1688
|
+
|
|
1689
|
+
1. **Writing Studio** (\`book_type: "book"\`) \u2014 free-form markdown file tree for novels and long-form prose. All paths are writable via MCP.
|
|
1690
|
+
|
|
1691
|
+
2. **Script Studio** (\`book_type: "script"\`) \u2014 structured schema for screenwriting with series / seasons / episodes / scenes / characters / locations / storylines / notes. Via MCP, only \`.script_studio/planning/**/*\` is writable; all structured files (series.json, *.character, *.scene, *.storyline, *.note, *.location, season.json, episode.json) and the \`.script_studio/planning/.initialized\` sentinel are READ-ONLY. Structural edits must happen through the Slima app UI.
|
|
1692
|
+
|
|
1693
|
+
**Before writing to a book you haven't inspected this session:** call \`list_books\` or \`get_book\` to see \`book_type\`. For per-book write rules, read the resource \`slima://books/{book_token}/schema\`.
|
|
1694
|
+
|
|
1695
|
+
Reads (\`read_file\`, \`search_content\`, \`get_book_structure\`) work the same way for both studio types \u2014 AI coaching, critique, and analysis are fully supported even on Script Studio books.`;
|
|
1488
1696
|
|
|
1489
1697
|
// src/core/utils/logger.ts
|
|
1490
1698
|
var isDebug = process.env.DEBUG === "true" || process.env.DEBUG === "1";
|
|
@@ -1521,14 +1729,20 @@ function createSlimaServer(config) {
|
|
|
1521
1729
|
getToken: async () => config.apiToken,
|
|
1522
1730
|
logger
|
|
1523
1731
|
});
|
|
1524
|
-
const server = new McpServer(
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1732
|
+
const server = new McpServer(
|
|
1733
|
+
{
|
|
1734
|
+
name: "slima",
|
|
1735
|
+
version: "0.2.0"
|
|
1736
|
+
},
|
|
1737
|
+
{
|
|
1738
|
+
instructions: SERVER_INSTRUCTIONS
|
|
1739
|
+
}
|
|
1740
|
+
);
|
|
1528
1741
|
registerBookTools(server, client);
|
|
1529
1742
|
registerContentTools(server, client);
|
|
1530
1743
|
registerBetaReaderTools(server, client, logger);
|
|
1531
1744
|
registerFileTools(server, client);
|
|
1745
|
+
registerBookSchemaResource(server, client);
|
|
1532
1746
|
logger.info("Slima MCP Server initialized");
|
|
1533
1747
|
logger.info(`API endpoint: ${config.baseUrl || "https://api.slima.ai"}`);
|
|
1534
1748
|
return server;
|
|
@@ -1975,7 +2189,7 @@ async function runStatus() {
|
|
|
1975
2189
|
}
|
|
1976
2190
|
|
|
1977
2191
|
// src/cli/index.ts
|
|
1978
|
-
var VERSION = "0.
|
|
2192
|
+
var VERSION = "0.2.0";
|
|
1979
2193
|
function showHelp() {
|
|
1980
2194
|
console.log(`
|
|
1981
2195
|
Slima MCP v${VERSION}
|