@udondan/dsbmobile 1.1.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/LICENSE +21 -0
- package/README.md +251 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +94 -0
- package/dist/constants.d.ts +21 -0
- package/dist/constants.js +21 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/mcp.d.ts +2 -0
- package/dist/mcp.js +18 -0
- package/dist/services/dsbmobile.d.ts +81 -0
- package/dist/services/dsbmobile.js +341 -0
- package/dist/tools/documents.d.ts +8 -0
- package/dist/tools/documents.js +80 -0
- package/dist/tools/news.d.ts +8 -0
- package/dist/tools/news.js +88 -0
- package/dist/tools/substitutions.d.ts +9 -0
- package/dist/tools/substitutions.js +164 -0
- package/dist/tools/timetables.d.ts +9 -0
- package/dist/tools/timetables.js +91 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.js +4 -0
- package/dist/utils/errors.d.ts +9 -0
- package/dist/utils/errors.js +50 -0
- package/package.json +82 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CHARACTER_LIMIT, ENV_CLASS } from '../constants.js';
|
|
3
|
+
/**
|
|
4
|
+
* Registers the get_substitutions tool with the MCP server.
|
|
5
|
+
*
|
|
6
|
+
* This tool fetches and parses the actual substitution plan HTML pages,
|
|
7
|
+
* returning structured substitution entries grouped by class.
|
|
8
|
+
*/
|
|
9
|
+
export function registerSubstitutionsTool(server, client) {
|
|
10
|
+
server.registerTool('get_substitutions', {
|
|
11
|
+
title: 'Get DSBmobile Substitution Entries',
|
|
12
|
+
description: `Fetches and parses the actual substitution plan (Vertretungsplan) pages from DSBmobile,
|
|
13
|
+
returning structured substitution entries for each class.
|
|
14
|
+
|
|
15
|
+
Returns a list of substitution plans (one per page), each containing:
|
|
16
|
+
- title: Plan name (e.g., "V-Homepage heute - subst_001 (Seite 1)")
|
|
17
|
+
- planDate: Date shown on the plan (e.g., "20.3.2026 Freitag (Seite 1 / 8)")
|
|
18
|
+
- lastUpdated: When the plan was last updated
|
|
19
|
+
- affectedClasses: Comma-separated list of affected class names
|
|
20
|
+
- entries: Array of substitution entries, each with:
|
|
21
|
+
- className: The class (e.g., "05b", "Q2_Kra")
|
|
22
|
+
- type: Substitution type (e.g., "Vertretung", "Statt-Vertretung", "Entfall")
|
|
23
|
+
- period: Lesson period(s) (e.g., "3" or "5 - 6")
|
|
24
|
+
- originalTeacher: Abbreviation of the absent teacher
|
|
25
|
+
- substituteTeacher: Abbreviation of the substitute teacher
|
|
26
|
+
- subject: Subject abbreviation (e.g., "SPO", "ETHI", "E")
|
|
27
|
+
- originalRoom: Original room
|
|
28
|
+
- substituteRoom: Substitute room
|
|
29
|
+
- text: Additional notes
|
|
30
|
+
|
|
31
|
+
Use this tool when:
|
|
32
|
+
- A user asks "Do I have any substitutions today?"
|
|
33
|
+
- A user wants to know which teacher is substituting for whom
|
|
34
|
+
- A user asks "Is lesson X cancelled?"
|
|
35
|
+
- A user wants to filter substitutions by class name
|
|
36
|
+
|
|
37
|
+
This tool downloads and parses all plan pages, which may take a few seconds.
|
|
38
|
+
For just the list of plan URLs, use get_timetables instead.
|
|
39
|
+
|
|
40
|
+
The className parameter is optional. If omitted, the DSB_CLASS environment variable
|
|
41
|
+
is used as the default filter. If neither is set, all classes are returned.
|
|
42
|
+
|
|
43
|
+
Error handling:
|
|
44
|
+
- Returns an error message if authentication fails (check DSB_USERNAME and DSB_PASSWORD)
|
|
45
|
+
- Returns an error message if the DSBmobile service is unavailable`,
|
|
46
|
+
inputSchema: z.object({
|
|
47
|
+
className: z
|
|
48
|
+
.string()
|
|
49
|
+
.optional()
|
|
50
|
+
.describe(`Optional: filter results to a specific class name (e.g. '05b', 'Q2_Kra'). Case-insensitive. Defaults to the DSB_CLASS environment variable if set.`),
|
|
51
|
+
}),
|
|
52
|
+
annotations: {
|
|
53
|
+
readOnlyHint: true,
|
|
54
|
+
destructiveHint: false,
|
|
55
|
+
idempotentHint: true,
|
|
56
|
+
openWorldHint: true,
|
|
57
|
+
},
|
|
58
|
+
}, async ({ className }) => {
|
|
59
|
+
try {
|
|
60
|
+
const plans = await client.getSubstitutions();
|
|
61
|
+
// Use the provided className, fall back to DSB_CLASS env var, or show all
|
|
62
|
+
const effectiveClass = className ?? process.env[ENV_CLASS];
|
|
63
|
+
if (plans.length === 0) {
|
|
64
|
+
return {
|
|
65
|
+
content: [
|
|
66
|
+
{
|
|
67
|
+
type: 'text',
|
|
68
|
+
text: 'Aktuell sind keine Vertretungspläne auf DSBmobile verfügbar.',
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
// Apply class filter if provided (explicit param takes priority over env var)
|
|
74
|
+
const filter = effectiveClass?.toLowerCase();
|
|
75
|
+
const filtered = filter
|
|
76
|
+
? plans.map((plan) => ({
|
|
77
|
+
...plan,
|
|
78
|
+
entries: plan.entries.filter((entry) => entry.className.toLowerCase().includes(filter)),
|
|
79
|
+
}))
|
|
80
|
+
: plans;
|
|
81
|
+
const totalEntries = filtered.reduce((sum, p) => sum + p.entries.length, 0);
|
|
82
|
+
if (filter && totalEntries === 0) {
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: 'text',
|
|
87
|
+
text: `Keine Vertretungen für Klasse "${effectiveClass}" gefunden.`,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const output = {
|
|
93
|
+
planCount: filtered.length,
|
|
94
|
+
totalEntries,
|
|
95
|
+
plans: filtered,
|
|
96
|
+
};
|
|
97
|
+
const text = formatSubstitutions(filtered, effectiveClass);
|
|
98
|
+
const finalText = text.length > CHARACTER_LIMIT
|
|
99
|
+
? text.slice(0, CHARACTER_LIMIT) +
|
|
100
|
+
'\n\n[Response truncated. Use the className filter to narrow results.]'
|
|
101
|
+
: text;
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: 'text', text: finalText }],
|
|
104
|
+
structuredContent: output,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : 'An unexpected error occurred.';
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: 'text', text: message }],
|
|
111
|
+
isError: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Formats substitution plans as human-readable markdown.
|
|
118
|
+
*/
|
|
119
|
+
function formatSubstitutions(plans, filter) {
|
|
120
|
+
const lines = [];
|
|
121
|
+
const totalEntries = plans.reduce((sum, p) => sum + p.entries.length, 0);
|
|
122
|
+
const heading = filter
|
|
123
|
+
? `# Substitutions for class "${filter}" (${totalEntries} entries)`
|
|
124
|
+
: `# DSBmobile Substitution Plans (${totalEntries} total entries)`;
|
|
125
|
+
lines.push(heading, '');
|
|
126
|
+
for (const plan of plans) {
|
|
127
|
+
if (plan.entries.length === 0)
|
|
128
|
+
continue;
|
|
129
|
+
lines.push(`## ${plan.title}`, `**Date**: ${plan.planDate}`, `**Last Updated**: ${plan.lastUpdated}`);
|
|
130
|
+
if (plan.affectedClasses) {
|
|
131
|
+
lines.push(`**Affected Classes**: ${plan.affectedClasses}`);
|
|
132
|
+
}
|
|
133
|
+
lines.push('');
|
|
134
|
+
// Group entries by class
|
|
135
|
+
const byClass = new Map();
|
|
136
|
+
for (const entry of plan.entries) {
|
|
137
|
+
const list = byClass.get(entry.className) ?? [];
|
|
138
|
+
list.push(entry);
|
|
139
|
+
byClass.set(entry.className, list);
|
|
140
|
+
}
|
|
141
|
+
for (const [cls, entries] of byClass) {
|
|
142
|
+
lines.push(`### Class ${cls}`);
|
|
143
|
+
for (const substitution of entries) {
|
|
144
|
+
const teacher = substitution.originalTeacher && substitution.substituteTeacher
|
|
145
|
+
? `${substitution.originalTeacher} → ${substitution.substituteTeacher}`
|
|
146
|
+
: substitution.substituteTeacher || substitution.originalTeacher;
|
|
147
|
+
const room = substitution.originalRoom && substitution.substituteRoom
|
|
148
|
+
? `${substitution.originalRoom} → ${substitution.substituteRoom}`
|
|
149
|
+
: substitution.substituteRoom || substitution.originalRoom;
|
|
150
|
+
const parts = [
|
|
151
|
+
`**${substitution.type}**`,
|
|
152
|
+
`Period ${substitution.period}`,
|
|
153
|
+
substitution.subject && `Subject: ${substitution.subject}`,
|
|
154
|
+
teacher && `Teacher: ${teacher}`,
|
|
155
|
+
room && `Room: ${room}`,
|
|
156
|
+
substitution.text && `Note: ${substitution.text}`,
|
|
157
|
+
].filter(Boolean);
|
|
158
|
+
lines.push(`- ${parts.join(' | ')}`);
|
|
159
|
+
}
|
|
160
|
+
lines.push('');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return lines.join('\n');
|
|
164
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { DsbmobileClient } from '../services/dsbmobile.js';
|
|
3
|
+
/**
|
|
4
|
+
* Registers the get_timetables tool with the MCP server.
|
|
5
|
+
*
|
|
6
|
+
* This tool retrieves all available substitution plan (Vertretungsplan) entries
|
|
7
|
+
* from DSBmobile, including URLs to the HTML plan pages.
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerTimetablesTool(server: McpServer, client: DsbmobileClient): void;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CHARACTER_LIMIT } from '../constants.js';
|
|
3
|
+
/**
|
|
4
|
+
* Registers the get_timetables tool with the MCP server.
|
|
5
|
+
*
|
|
6
|
+
* This tool retrieves all available substitution plan (Vertretungsplan) entries
|
|
7
|
+
* from DSBmobile, including URLs to the HTML plan pages.
|
|
8
|
+
*/
|
|
9
|
+
export function registerTimetablesTool(server, client) {
|
|
10
|
+
server.registerTool('get_timetables', {
|
|
11
|
+
title: 'Get DSBmobile Substitution Plans',
|
|
12
|
+
description: `Retrieves all available substitution plan (Vertretungsplan) entries from DSBmobile.
|
|
13
|
+
|
|
14
|
+
Returns a list of substitution plan entries, each with:
|
|
15
|
+
- id: Unique identifier for the plan entry
|
|
16
|
+
- title: Plan name (e.g., "Vertretungen-heute" for today's substitutions)
|
|
17
|
+
- date: Last updated timestamp in DD.MM.YYYY HH:MM format
|
|
18
|
+
- url: URL to the HTML page containing the actual substitution plan table
|
|
19
|
+
- previewUrl: URL to a preview image of the plan (optional)
|
|
20
|
+
|
|
21
|
+
The substitution plan HTML pages contain the detailed schedule showing which
|
|
22
|
+
classes have substitutions, which teachers are replaced, which rooms are used, etc.
|
|
23
|
+
The HTML format varies by school, so the URL is returned for further processing.
|
|
24
|
+
|
|
25
|
+
Use this tool when:
|
|
26
|
+
- A user asks "Do I have any substitutions today?"
|
|
27
|
+
- A user wants to check the school's substitution schedule
|
|
28
|
+
- A user asks about teacher absences or room changes
|
|
29
|
+
|
|
30
|
+
Returns "No substitution plans available" if no plans are currently published.
|
|
31
|
+
|
|
32
|
+
Error handling:
|
|
33
|
+
- Returns an error message if authentication fails (check DSB_USERNAME and DSB_PASSWORD)
|
|
34
|
+
- Returns an error message if the DSBmobile service is unavailable`,
|
|
35
|
+
inputSchema: z.object({}).strict(),
|
|
36
|
+
annotations: {
|
|
37
|
+
readOnlyHint: true,
|
|
38
|
+
destructiveHint: false,
|
|
39
|
+
idempotentHint: true,
|
|
40
|
+
openWorldHint: true,
|
|
41
|
+
},
|
|
42
|
+
}, async () => {
|
|
43
|
+
try {
|
|
44
|
+
const entries = await client.getTimetables();
|
|
45
|
+
if (entries.length === 0) {
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: 'text',
|
|
50
|
+
text: 'Aktuell sind keine Vertretungspläne auf DSBmobile verfügbar.',
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const output = {
|
|
56
|
+
count: entries.length,
|
|
57
|
+
timetables: entries,
|
|
58
|
+
};
|
|
59
|
+
const text = formatTimetables(entries);
|
|
60
|
+
const finalText = text.length > CHARACTER_LIMIT
|
|
61
|
+
? text.slice(0, CHARACTER_LIMIT) +
|
|
62
|
+
'\n\n[Response truncated. Use the plan URLs to access the full content.]'
|
|
63
|
+
: text;
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: 'text', text: finalText }],
|
|
66
|
+
structuredContent: output,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const message = error instanceof Error ? error.message : 'An unexpected error occurred.';
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: 'text', text: message }],
|
|
73
|
+
isError: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Formats timetable entries as human-readable markdown.
|
|
80
|
+
*/
|
|
81
|
+
function formatTimetables(entries) {
|
|
82
|
+
const lines = [`# DSBmobile Substitution Plans (${entries.length} available)`, ''];
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
lines.push(`## ${entry.title}`, `- **ID**: ${entry.id}`, `- **Last Updated**: ${entry.date}`, `- **Plan URL**: ${entry.url}`);
|
|
85
|
+
if (entry.previewUrl) {
|
|
86
|
+
lines.push(`- **Preview**: ${entry.previewUrl}`);
|
|
87
|
+
}
|
|
88
|
+
lines.push('');
|
|
89
|
+
}
|
|
90
|
+
return lines.join('\n');
|
|
91
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript interfaces for DSBmobile API data structures
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Raw item structure returned by the DSBmobile API.
|
|
6
|
+
* ConType determines how data is encoded:
|
|
7
|
+
* - 2: Data is in Childs array (nested items)
|
|
8
|
+
* - 4: Detail contains a link to an HTML web page
|
|
9
|
+
* - 5: Detail contains plain text
|
|
10
|
+
* - 6: Detail contains a link to an image/file
|
|
11
|
+
*/
|
|
12
|
+
export interface DsbItem {
|
|
13
|
+
Id: string;
|
|
14
|
+
Date: string;
|
|
15
|
+
Title: string;
|
|
16
|
+
Detail: string;
|
|
17
|
+
Tags: string;
|
|
18
|
+
ConType: number;
|
|
19
|
+
Prio: number;
|
|
20
|
+
Index: number;
|
|
21
|
+
Childs: DsbItem[];
|
|
22
|
+
Preview: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A processed timetable/substitution plan entry
|
|
26
|
+
*/
|
|
27
|
+
export interface TimetableEntry {
|
|
28
|
+
/** Unique identifier */
|
|
29
|
+
id: string;
|
|
30
|
+
/** Plan name (e.g., "Vertretungen-heute") */
|
|
31
|
+
title: string;
|
|
32
|
+
/** Last updated date in DD.MM.YYYY HH:MM format */
|
|
33
|
+
date: string;
|
|
34
|
+
/** URL to the HTML plan page */
|
|
35
|
+
url: string;
|
|
36
|
+
/** URL to the preview image (if available) */
|
|
37
|
+
previewUrl?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A processed news/announcement entry
|
|
41
|
+
*/
|
|
42
|
+
export interface NewsEntry {
|
|
43
|
+
/** Unique identifier */
|
|
44
|
+
id: string;
|
|
45
|
+
/** News headline */
|
|
46
|
+
title: string;
|
|
47
|
+
/** News content or URL */
|
|
48
|
+
detail: string;
|
|
49
|
+
/** Publication date in DD.MM.YYYY HH:MM format */
|
|
50
|
+
date: string;
|
|
51
|
+
/** Associated tags */
|
|
52
|
+
tags: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A single substitution entry parsed from a timetable HTML page
|
|
56
|
+
*/
|
|
57
|
+
export interface SubstitutionEntry {
|
|
58
|
+
/** Class name (e.g., "05b") */
|
|
59
|
+
className: string;
|
|
60
|
+
/** Type of substitution (e.g., "Vertretung", "Statt-Vertretung", "Entfall") */
|
|
61
|
+
type: string;
|
|
62
|
+
/** Lesson period(s) (e.g., "3" or "5 - 6") */
|
|
63
|
+
period: string;
|
|
64
|
+
/** Original teacher abbreviation */
|
|
65
|
+
originalTeacher: string;
|
|
66
|
+
/** Substitute teacher abbreviation */
|
|
67
|
+
substituteTeacher: string;
|
|
68
|
+
/** Subject */
|
|
69
|
+
subject: string;
|
|
70
|
+
/** Original room */
|
|
71
|
+
originalRoom: string;
|
|
72
|
+
/** Substitute room */
|
|
73
|
+
substituteRoom: string;
|
|
74
|
+
/** Additional notes */
|
|
75
|
+
text: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A fully parsed substitution plan page
|
|
79
|
+
*/
|
|
80
|
+
export interface SubstitutionPlan {
|
|
81
|
+
/** Plan title including date (e.g., "V-Homepage heute - subst_001") */
|
|
82
|
+
title: string;
|
|
83
|
+
/** Date string from the plan (e.g., "20.3.2026 Freitag (Seite 1 / 8)") */
|
|
84
|
+
planDate: string;
|
|
85
|
+
/** Last updated timestamp */
|
|
86
|
+
lastUpdated: string;
|
|
87
|
+
/** URL the plan was fetched from */
|
|
88
|
+
url: string;
|
|
89
|
+
/** Affected classes */
|
|
90
|
+
affectedClasses: string;
|
|
91
|
+
/** All substitution entries */
|
|
92
|
+
entries: SubstitutionEntry[];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* A processed document entry
|
|
96
|
+
*/
|
|
97
|
+
export interface DocumentEntry {
|
|
98
|
+
/** Unique identifier */
|
|
99
|
+
id: string;
|
|
100
|
+
/** Document name */
|
|
101
|
+
title: string;
|
|
102
|
+
/** Download URL */
|
|
103
|
+
url: string;
|
|
104
|
+
/** Upload date in DD.MM.YYYY HH:MM format */
|
|
105
|
+
date: string;
|
|
106
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts an API error into a human-readable, actionable error message.
|
|
3
|
+
* Credentials are never included in error messages.
|
|
4
|
+
*/
|
|
5
|
+
export declare function handleApiError(error: unknown): string;
|
|
6
|
+
/**
|
|
7
|
+
* Creates an error message for authentication failure (empty token response).
|
|
8
|
+
*/
|
|
9
|
+
export declare function createAuthError(): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { AxiosError } from 'axios';
|
|
2
|
+
/**
|
|
3
|
+
* Converts an API error into a human-readable, actionable error message.
|
|
4
|
+
* Credentials are never included in error messages.
|
|
5
|
+
*/
|
|
6
|
+
export function handleApiError(error) {
|
|
7
|
+
if (error instanceof AxiosError) {
|
|
8
|
+
if (error.response) {
|
|
9
|
+
switch (error.response.status) {
|
|
10
|
+
case 401: {
|
|
11
|
+
return 'Error: Authentication failed. Please verify your credentials are correct.';
|
|
12
|
+
}
|
|
13
|
+
case 403: {
|
|
14
|
+
return 'Error: Access denied. Your account may not have permission to access this resource.';
|
|
15
|
+
}
|
|
16
|
+
case 404: {
|
|
17
|
+
return 'Error: Resource not found. The DSBmobile API endpoint may have changed.';
|
|
18
|
+
}
|
|
19
|
+
case 429: {
|
|
20
|
+
return 'Error: Rate limit exceeded. Please wait a moment before making more requests.';
|
|
21
|
+
}
|
|
22
|
+
case 500:
|
|
23
|
+
case 502:
|
|
24
|
+
case 503:
|
|
25
|
+
case 504: {
|
|
26
|
+
return 'Error: DSBmobile service is temporarily unavailable. Please try again later.';
|
|
27
|
+
}
|
|
28
|
+
default: {
|
|
29
|
+
return `Error: API request failed with status ${error.response.status}. Please try again.`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
|
34
|
+
return 'Error: Request timed out. The DSBmobile service may be slow or unavailable. Please try again.';
|
|
35
|
+
}
|
|
36
|
+
else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
|
|
37
|
+
return 'Error: Cannot connect to DSBmobile. Please check your internet connection and try again.';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (error instanceof Error) {
|
|
41
|
+
return `Error: ${error.message}`;
|
|
42
|
+
}
|
|
43
|
+
return 'Error: An unexpected error occurred. Please try again.';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Creates an error message for authentication failure (empty token response).
|
|
47
|
+
*/
|
|
48
|
+
export function createAuthError() {
|
|
49
|
+
return 'Error: Authentication failed. DSBmobile rejected the provided credentials. Please verify your credentials are correct.';
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@udondan/dsbmobile",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "SDK, CLI, and MCP server for DSBmobile — access German school substitution plans (Vertretungspläne)",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/udondan/dsbmobile-mcp.git"
|
|
8
|
+
},
|
|
9
|
+
"author": {
|
|
10
|
+
"name": "Daniel Schroeder",
|
|
11
|
+
"email": "deemes79@googlemail.com",
|
|
12
|
+
"url": "https://udondan.com/"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"funding": {
|
|
16
|
+
"type": "github",
|
|
17
|
+
"url": "https://github.com/sponsors/udondan"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"mcp-server",
|
|
22
|
+
"cli",
|
|
23
|
+
"sdk",
|
|
24
|
+
"dsbmobile",
|
|
25
|
+
"vertretungsplan",
|
|
26
|
+
"school",
|
|
27
|
+
"substitution-plan"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "dist/index.js",
|
|
31
|
+
"types": "dist/index.d.ts",
|
|
32
|
+
"bin": {
|
|
33
|
+
"dsbmobile": "dist/cli.js"
|
|
34
|
+
},
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"import": "./dist/index.js",
|
|
38
|
+
"types": "./dist/index.d.ts"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc",
|
|
47
|
+
"dev": "tsc --watch",
|
|
48
|
+
"start": "node dist/cli.js mcp",
|
|
49
|
+
"pretest": "tsc",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:watch": "vitest",
|
|
52
|
+
"typecheck": "tsc --noEmit -p tsconfig.lint.json",
|
|
53
|
+
"lint": "eslint src/ tests/",
|
|
54
|
+
"lint:fix": "eslint --fix src/ tests/",
|
|
55
|
+
"format": "prettier --write src/ tests/",
|
|
56
|
+
"format:check": "prettier --check src/ tests/",
|
|
57
|
+
"markdownlint": "markdownlint-cli2 '**/*.md' '#node_modules' '#.claude' '#.vibe' '#CHANGELOG.md'"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=22"
|
|
61
|
+
},
|
|
62
|
+
"dependencies": {
|
|
63
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
64
|
+
"axios": "1.16.1",
|
|
65
|
+
"commander": "14.0.3",
|
|
66
|
+
"zod": "4.4.3"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@eslint/js": "10.0.1",
|
|
70
|
+
"@modelcontextprotocol/inspector": "0.21.2",
|
|
71
|
+
"@types/node": "22.0.0",
|
|
72
|
+
"eslint": "10.4.0",
|
|
73
|
+
"eslint-config-prettier": "10.1.8",
|
|
74
|
+
"eslint-plugin-prettier": "5.5.5",
|
|
75
|
+
"eslint-plugin-unicorn": "64.0.0",
|
|
76
|
+
"prettier": "3.8.3",
|
|
77
|
+
"typescript": "6.0.3",
|
|
78
|
+
"typescript-eslint": "8.59.4",
|
|
79
|
+
"markdownlint-cli2": "0.22.1",
|
|
80
|
+
"vitest": "4.1.7"
|
|
81
|
+
}
|
|
82
|
+
}
|