slima-mcp 0.1.14 → 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 +249 -33
- 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
|
*/
|
|
@@ -132,7 +150,7 @@ var SlimaApiClient = class {
|
|
|
132
150
|
* List all books
|
|
133
151
|
*/
|
|
134
152
|
async listBooks() {
|
|
135
|
-
return this.request("GET", "/api/v1/books");
|
|
153
|
+
return this.request("GET", "/api/v1/books?status=active");
|
|
136
154
|
}
|
|
137
155
|
/**
|
|
138
156
|
* Get book details
|
|
@@ -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"}
|
|
@@ -507,8 +547,12 @@ ${formatted}`
|
|
|
507
547
|
- **Language**: ${book.language || "N/A"}
|
|
508
548
|
- **Total Words**: ${book.totalWordCount?.toLocaleString() || "0"}
|
|
509
549
|
- **Manuscript Words**: ${book.manuscriptWordCount?.toLocaleString() || "0"}
|
|
550
|
+
- **Reference Words**: ${book.referenceWordCount?.toLocaleString() || "0"}
|
|
510
551
|
- **Created**: ${book.createdAt}
|
|
511
552
|
- **Updated**: ${book.updatedAt}`;
|
|
553
|
+
const text = book.bookType === "script" ? `${header}
|
|
554
|
+
|
|
555
|
+
${scriptStudioWriteRulesBlock(book)}` : header;
|
|
512
556
|
return {
|
|
513
557
|
content: [{ type: "text", text }]
|
|
514
558
|
};
|
|
@@ -529,7 +573,10 @@ ${formatted}`
|
|
|
529
573
|
{ readOnlyHint: true, openWorldHint: true },
|
|
530
574
|
async ({ book_token }) => {
|
|
531
575
|
try {
|
|
532
|
-
const commits = await
|
|
576
|
+
const [book, commits] = await Promise.all([
|
|
577
|
+
client.getBook(book_token),
|
|
578
|
+
client.listCommits(book_token, 1)
|
|
579
|
+
]);
|
|
533
580
|
if (commits.length === 0) {
|
|
534
581
|
return {
|
|
535
582
|
content: [
|
|
@@ -543,13 +590,16 @@ ${formatted}`
|
|
|
543
590
|
const flatSnapshot = commits[0].filesSnapshot;
|
|
544
591
|
const nestedTree = buildFileTree(flatSnapshot);
|
|
545
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\`._` : "";
|
|
546
596
|
return {
|
|
547
597
|
content: [
|
|
548
598
|
{
|
|
549
599
|
type: "text",
|
|
550
|
-
text: `File structure for book ${book_token}:
|
|
600
|
+
text: `File structure for ${studioLabel(book)} \u2014 ${book.title} (${book_token}):
|
|
551
601
|
|
|
552
|
-
${tree}`
|
|
602
|
+
${tree}${footer}`
|
|
553
603
|
}
|
|
554
604
|
]
|
|
555
605
|
};
|
|
@@ -586,6 +636,7 @@ ${tree}`
|
|
|
586
636
|
|
|
587
637
|
- **Total Words**: ${book.totalWordCount?.toLocaleString() || "0"}
|
|
588
638
|
- **Manuscript Words**: ${book.manuscriptWordCount?.toLocaleString() || "0"}
|
|
639
|
+
- **Reference Words**: ${book.referenceWordCount?.toLocaleString() || "0"}
|
|
589
640
|
- **Recent Progress**: ${progressText}
|
|
590
641
|
- **Commits Analyzed**: ${commits.length}
|
|
591
642
|
- **Language**: ${book.language || "N/A"}`;
|
|
@@ -824,15 +875,31 @@ ${formatted}
|
|
|
824
875
|
);
|
|
825
876
|
server.tool(
|
|
826
877
|
"analyze_chapter",
|
|
827
|
-
|
|
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.`,
|
|
828
881
|
{
|
|
829
|
-
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").'),
|
|
830
883
|
file_path: z3.string().describe("Chapter file path (e.g., /chapters/01.md)"),
|
|
831
884
|
persona_token: z3.string().describe("Persona token (e.g., psn_xxx). Use list_personas to find available personas.")
|
|
832
885
|
},
|
|
833
886
|
{ readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
834
887
|
async ({ book_token, file_path, persona_token }) => {
|
|
835
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
|
+
}
|
|
836
903
|
const commits = await client.listCommits(book_token, 1);
|
|
837
904
|
if (commits.length === 0) {
|
|
838
905
|
return {
|
|
@@ -1177,6 +1244,18 @@ function searchByName2(files, name) {
|
|
|
1177
1244
|
|
|
1178
1245
|
// src/core/tools/files.ts
|
|
1179
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"];
|
|
1180
1259
|
function registerFileTools(server, client) {
|
|
1181
1260
|
server.tool(
|
|
1182
1261
|
"read_file",
|
|
@@ -1213,10 +1292,11 @@ function registerFileTools(server, client) {
|
|
|
1213
1292
|
);
|
|
1214
1293
|
server.tool(
|
|
1215
1294
|
"write_file",
|
|
1216
|
-
|
|
1295
|
+
`Replace the entire content of an existing file (creates a new commit).
|
|
1296
|
+
${SCRIPT_STUDIO_WRITE_WARNING}`,
|
|
1217
1297
|
{
|
|
1218
1298
|
book_token: z4.string().describe("Book token (e.g., bk_abc123)"),
|
|
1219
|
-
path: z4.string().describe(
|
|
1299
|
+
path: z4.string().describe(PATH_PARAM_HINT),
|
|
1220
1300
|
content: z4.string().describe("New content to write"),
|
|
1221
1301
|
commit_message: z4.string().optional().describe("Commit message (optional, auto-generated if not provided)")
|
|
1222
1302
|
},
|
|
@@ -1249,10 +1329,11 @@ Message: ${result.commit.name || result.commit.message}`
|
|
|
1249
1329
|
);
|
|
1250
1330
|
server.tool(
|
|
1251
1331
|
"edit_file",
|
|
1252
|
-
|
|
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}`,
|
|
1253
1334
|
{
|
|
1254
1335
|
book_token: z4.string().describe("Book token (e.g., bk_abc123)"),
|
|
1255
|
-
path: z4.string().describe(
|
|
1336
|
+
path: z4.string().describe(PATH_PARAM_HINT),
|
|
1256
1337
|
old_string: z4.string().describe("The exact text to find and replace (must match exactly, including whitespace)"),
|
|
1257
1338
|
new_string: z4.string().describe("The new text to replace it with"),
|
|
1258
1339
|
replace_all: z4.boolean().optional().describe("Replace all occurrences (default: false, replaces only first match)"),
|
|
@@ -1324,10 +1405,13 @@ ${old_string.slice(0, 200)}${old_string.length > 200 ? "..." : ""}
|
|
|
1324
1405
|
);
|
|
1325
1406
|
server.tool(
|
|
1326
1407
|
"create_file",
|
|
1327
|
-
|
|
1408
|
+
`Create a new file in a book (creates a new commit).
|
|
1409
|
+
${SCRIPT_STUDIO_WRITE_WARNING}`,
|
|
1328
1410
|
{
|
|
1329
1411
|
book_token: z4.string().describe("Book token (e.g., bk_abc123)"),
|
|
1330
|
-
path: z4.string().describe(
|
|
1412
|
+
path: z4.string().describe(
|
|
1413
|
+
`${PATH_PARAM_HINT} Parent folders are auto-created (mkdir -p behavior).`
|
|
1414
|
+
),
|
|
1331
1415
|
content: z4.string().optional().describe("Initial content (optional, defaults to empty)"),
|
|
1332
1416
|
commit_message: z4.string().optional().describe("Commit message (optional, auto-generated if not provided)")
|
|
1333
1417
|
},
|
|
@@ -1361,10 +1445,11 @@ Message: ${result.commit.name || result.commit.message}`
|
|
|
1361
1445
|
);
|
|
1362
1446
|
server.tool(
|
|
1363
1447
|
"delete_file",
|
|
1364
|
-
|
|
1448
|
+
`Delete a file from a book (creates a new commit).
|
|
1449
|
+
${SCRIPT_STUDIO_WRITE_WARNING}`,
|
|
1365
1450
|
{
|
|
1366
1451
|
book_token: z4.string().describe("Book token (e.g., bk_abc123)"),
|
|
1367
|
-
path: z4.string().describe(
|
|
1452
|
+
path: z4.string().describe(PATH_PARAM_HINT),
|
|
1368
1453
|
commit_message: z4.string().optional().describe("Commit message (optional, auto-generated if not provided)")
|
|
1369
1454
|
},
|
|
1370
1455
|
{ readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
|
|
@@ -1395,10 +1480,11 @@ Message: ${result.commit.name || result.commit.message}`
|
|
|
1395
1480
|
);
|
|
1396
1481
|
server.tool(
|
|
1397
1482
|
"append_to_file",
|
|
1398
|
-
|
|
1483
|
+
`Append content to the end of an existing file (creates a new commit).
|
|
1484
|
+
${SCRIPT_STUDIO_WRITE_WARNING}`,
|
|
1399
1485
|
{
|
|
1400
1486
|
book_token: z4.string().describe("Book token (e.g., bk_abc123)"),
|
|
1401
|
-
path: z4.string().describe(
|
|
1487
|
+
path: z4.string().describe(PATH_PARAM_HINT),
|
|
1402
1488
|
content: z4.string().describe("Content to append"),
|
|
1403
1489
|
commit_message: z4.string().optional().describe("Commit message (optional, auto-generated if not provided)")
|
|
1404
1490
|
},
|
|
@@ -1431,27 +1517,45 @@ Message: ${result.commit.name || result.commit.message}`
|
|
|
1431
1517
|
);
|
|
1432
1518
|
server.tool(
|
|
1433
1519
|
"search_content",
|
|
1434
|
-
|
|
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.`,
|
|
1435
1525
|
{
|
|
1436
1526
|
book_token: z4.string().describe("Book token (e.g., bk_abc123)"),
|
|
1437
1527
|
query: z4.string().describe('Search query (e.g., "blue eyes" or "protagonist name")'),
|
|
1438
1528
|
file_types: z4.array(z4.string()).optional().describe('Filter by file types (e.g., ["markdown", "txt"])'),
|
|
1439
|
-
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
|
+
)
|
|
1440
1533
|
},
|
|
1441
1534
|
{ readOnlyHint: true, openWorldHint: true },
|
|
1442
|
-
async ({ book_token, query, file_types, limit }) => {
|
|
1535
|
+
async ({ book_token, query, file_types, limit, include_structured }) => {
|
|
1443
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
|
+
}
|
|
1444
1545
|
const result = await client.searchFiles(book_token, {
|
|
1445
1546
|
query,
|
|
1446
1547
|
fileTypes: file_types,
|
|
1447
1548
|
limit
|
|
1448
1549
|
});
|
|
1449
|
-
|
|
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)` : "";
|
|
1450
1554
|
return {
|
|
1451
1555
|
content: [
|
|
1452
1556
|
{
|
|
1453
1557
|
type: "text",
|
|
1454
|
-
text: `No matches found for "${query}".`
|
|
1558
|
+
text: `No matches found for "${query}"${suffix}.`
|
|
1455
1559
|
}
|
|
1456
1560
|
]
|
|
1457
1561
|
};
|
|
@@ -1459,10 +1563,17 @@ Message: ${result.commit.name || result.commit.message}`
|
|
|
1459
1563
|
const output = [
|
|
1460
1564
|
`# Search Results for "${query}"`,
|
|
1461
1565
|
"",
|
|
1462
|
-
`Found ${
|
|
1566
|
+
`Found ${filteredMatches.length} file(s) with matches:`,
|
|
1463
1567
|
""
|
|
1464
1568
|
];
|
|
1465
|
-
|
|
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) {
|
|
1466
1577
|
output.push(`## ${match.file.path}`);
|
|
1467
1578
|
output.push(`Words: ${match.file.wordCount} | Matches: ${match.matchCount}`);
|
|
1468
1579
|
output.push("");
|
|
@@ -1483,6 +1594,105 @@ Message: ${result.commit.name || result.commit.message}`
|
|
|
1483
1594
|
}
|
|
1484
1595
|
);
|
|
1485
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.`;
|
|
1486
1696
|
|
|
1487
1697
|
// src/core/utils/logger.ts
|
|
1488
1698
|
var isDebug = process.env.DEBUG === "true" || process.env.DEBUG === "1";
|
|
@@ -1519,14 +1729,20 @@ function createSlimaServer(config) {
|
|
|
1519
1729
|
getToken: async () => config.apiToken,
|
|
1520
1730
|
logger
|
|
1521
1731
|
});
|
|
1522
|
-
const server = new McpServer(
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1732
|
+
const server = new McpServer(
|
|
1733
|
+
{
|
|
1734
|
+
name: "slima",
|
|
1735
|
+
version: "0.2.0"
|
|
1736
|
+
},
|
|
1737
|
+
{
|
|
1738
|
+
instructions: SERVER_INSTRUCTIONS
|
|
1739
|
+
}
|
|
1740
|
+
);
|
|
1526
1741
|
registerBookTools(server, client);
|
|
1527
1742
|
registerContentTools(server, client);
|
|
1528
1743
|
registerBetaReaderTools(server, client, logger);
|
|
1529
1744
|
registerFileTools(server, client);
|
|
1745
|
+
registerBookSchemaResource(server, client);
|
|
1530
1746
|
logger.info("Slima MCP Server initialized");
|
|
1531
1747
|
logger.info(`API endpoint: ${config.baseUrl || "https://api.slima.ai"}`);
|
|
1532
1748
|
return server;
|
|
@@ -1973,7 +2189,7 @@ async function runStatus() {
|
|
|
1973
2189
|
}
|
|
1974
2190
|
|
|
1975
2191
|
// src/cli/index.ts
|
|
1976
|
-
var VERSION = "0.
|
|
2192
|
+
var VERSION = "0.2.0";
|
|
1977
2193
|
function showHelp() {
|
|
1978
2194
|
console.log(`
|
|
1979
2195
|
Slima MCP v${VERSION}
|