dashley-mcp-framework 1.0.1

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 ADDED
@@ -0,0 +1,70 @@
1
+ # mcp-chaos-auth
2
+
3
+ MCP server with authenticated access to Chaos Auth backend.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g mcp-chaos-auth
9
+ rm -rf /Users/joshua.icuss/mcp-servers/claude-mcp-minimal; mkdir /Users/joshua.icuss/mcp-servers/claude-mcp-minimal
10
+ cp -R /Users/joshua.icuss/Documents/claude-mcp-minimal/* /Users/joshua.icuss/mcp-servers/claude-mcp-minimal
11
+
12
+
13
+ Link it locally for development - If you want to test the CLI tool while developing, you can link it:
14
+
15
+
16
+ npm install
17
+ npm run build
18
+ npm link
19
+
20
+ /opt/homebrew/bin/npx
21
+
22
+ tasty-mcp-minimal
23
+
24
+
25
+
26
+ node /Users/joshua.icuss/mcp-servers/claude-mcp-minimal/dist/index.js
27
+ npm login
28
+
29
+ Update package.json:
30
+ File operations writeTFile operations writeResultDone
31
+
32
+ npm run build
33
+ npm publish
34
+
35
+ npm run build
36
+ npm unlink -g
37
+ npm link
38
+
39
+ mcp-chaos-auth-server
40
+
41
+
42
+ npm install mcp-chaos-auth
43
+
44
+ npm mcp-chaos-auth-server
45
+
46
+ **Option 1 - Global install:**
47
+ ```bash
48
+ npm install -g mcp-chaos-auth
49
+ mcp-chaos-auth-server
50
+ ```
51
+ ```bash
52
+ mcp-chaos-auth login
53
+ mcp-chaos-auth status
54
+ ```
55
+
56
+ Shows session expiry and permission count. Does not call backend.
57
+
58
+ ### Claude Desktop
59
+
60
+ Add to `claude_desktop_config.json`:
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "chaos-auth": {
66
+ "command": "tasty-mcp-server"
67
+ }
68
+ }
69
+ }
70
+ ```
@@ -0,0 +1,2 @@
1
+ import { AxiosInstance } from 'axios';
2
+ export declare function createClient(apiUrl: string): AxiosInstance;
@@ -0,0 +1,35 @@
1
+ import axios from 'axios';
2
+ import { readSession, deleteSession } from '../auth/session.js';
3
+ import { refreshIfNeeded } from '../auth/refresh.js';
4
+ export function createClient(apiUrl) {
5
+ const client = axios.create({
6
+ baseURL: apiUrl,
7
+ });
8
+ client.interceptors.request.use(async (config) => {
9
+ const session = readSession();
10
+ if (!session) {
11
+ throw new Error('Not logged in. Run: mcp-chaos-auth login');
12
+ }
13
+ const refreshedSession = await refreshIfNeeded(session, apiUrl);
14
+ if (!refreshedSession) {
15
+ deleteSession();
16
+ throw new Error('Session expired. Run: mcp-chaos-auth login');
17
+ }
18
+ config.headers.Authorization = `Bearer ${refreshedSession.token}`;
19
+ return config;
20
+ });
21
+ client.interceptors.response.use((response) => response, (error) => {
22
+ if (error.response?.status === 401) {
23
+ deleteSession();
24
+ throw new Error('Authentication failed. Please login again.');
25
+ }
26
+ if (error.response?.status === 403) {
27
+ throw new Error(`Insufficient permissions: ${error.response.data?.detail || 'Access denied'}`);
28
+ }
29
+ if (error.response?.status >= 500) {
30
+ throw new Error(`Server error: ${error.response.status}`);
31
+ }
32
+ throw error;
33
+ });
34
+ return client;
35
+ }
@@ -0,0 +1,7 @@
1
+ import type { AxiosInstance } from 'axios';
2
+ export declare function validateSession(client: AxiosInstance): Promise<{
3
+ username: string;
4
+ roles: string[];
5
+ permissions: string[];
6
+ }>;
7
+ export declare function getUserPermissions(client: AxiosInstance): Promise<string[]>;
@@ -0,0 +1,8 @@
1
+ export async function validateSession(client) {
2
+ const response = await client.get('/auth/validate');
3
+ return response.data;
4
+ }
5
+ export async function getUserPermissions(client) {
6
+ const response = await client.get('/users/me/permissions');
7
+ return response.data;
8
+ }
@@ -0,0 +1,9 @@
1
+ export interface ApiError {
2
+ error: string;
3
+ detail: string;
4
+ code: number;
5
+ }
6
+ export interface MFAChallengeRequest {
7
+ challenge_token: string;
8
+ code: string;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { Session } from './types.js';
2
+ export declare function login(apiUrl: string): Promise<Session>;
@@ -0,0 +1,112 @@
1
+ import * as readline from 'readline';
2
+ import axios from 'axios';
3
+ async function prompt(question) {
4
+ const rl = readline.createInterface({
5
+ input: process.stdin,
6
+ output: process.stdout,
7
+ });
8
+ return new Promise((resolve) => {
9
+ rl.question(question, (answer) => {
10
+ rl.close();
11
+ resolve(answer);
12
+ });
13
+ });
14
+ }
15
+ async function promptSecret(question) {
16
+ return new Promise((resolve) => {
17
+ const rl = readline.createInterface({
18
+ input: process.stdin,
19
+ output: process.stdout,
20
+ terminal: false,
21
+ });
22
+ const stdin = process.stdin;
23
+ stdin.setRawMode(true);
24
+ stdin.resume();
25
+ stdin.setEncoding('utf8');
26
+ process.stdout.write(question);
27
+ let password = '';
28
+ const onData = (char) => {
29
+ switch (char) {
30
+ case '\n':
31
+ case '\r':
32
+ case '\u0004':
33
+ stdin.setRawMode(false);
34
+ stdin.pause();
35
+ stdin.removeListener('data', onData);
36
+ process.stdout.write('\n');
37
+ resolve(password);
38
+ break;
39
+ case '\u0003':
40
+ stdin.setRawMode(false);
41
+ process.exit(1);
42
+ break;
43
+ case '\u007f':
44
+ if (password.length > 0) {
45
+ password = password.slice(0, -1);
46
+ process.stdout.write('\b \b');
47
+ }
48
+ break;
49
+ default:
50
+ if (char.charCodeAt(0) >= 32) {
51
+ password += char;
52
+ process.stdout.write('*');
53
+ }
54
+ break;
55
+ }
56
+ };
57
+ stdin.on('data', onData);
58
+ });
59
+ }
60
+ export async function login(apiUrl) {
61
+ const username = await prompt('Username: ');
62
+ const password = await promptSecret('Password: ');
63
+ try {
64
+ const loginResponse = await axios.post(`${apiUrl}/auth/login`, {
65
+ username,
66
+ password,
67
+ });
68
+ let accessToken = loginResponse.data.access_token;
69
+ let permissions = loginResponse.data.permissions || [];
70
+ if (loginResponse.data.mfa_required && loginResponse.data.challenge_token) {
71
+ const challengeToken = loginResponse.data.challenge_token;
72
+ for (let attempt = 1; attempt <= 3; attempt++) {
73
+ const mfaCode = await prompt('MFA Code: ');
74
+ try {
75
+ const mfaResponse = await axios.post(`${apiUrl}/auth/mfa/challenge`, {
76
+ challenge_token: challengeToken,
77
+ code: mfaCode,
78
+ });
79
+ accessToken = mfaResponse.data.access_token;
80
+ permissions = mfaResponse.data.permissions || [];
81
+ break;
82
+ }
83
+ catch (error) {
84
+ if (attempt === 3) {
85
+ throw new Error('MFA verification failed after 3 attempts');
86
+ }
87
+ console.error('Invalid MFA code, try again');
88
+ }
89
+ }
90
+ }
91
+ const validateResponse = await axios.get(`${apiUrl}/auth/validate`, {
92
+ headers: { Authorization: `Bearer ${accessToken}` },
93
+ });
94
+ const validatedPermissions = validateResponse.data.permissions || permissions;
95
+ const now = new Date();
96
+ return {
97
+ token: accessToken,
98
+ expiresAt: new Date(now.getTime() + loginResponse.data.expires_in * 1000),
99
+ loginAt: now,
100
+ permissions: validatedPermissions,
101
+ };
102
+ }
103
+ catch (error) {
104
+ if (error.response?.status === 401) {
105
+ throw new Error('Invalid credentials');
106
+ }
107
+ if (!error.response) {
108
+ throw new Error('Cannot reach auth server');
109
+ }
110
+ throw new Error(error.response?.data?.detail || error.message);
111
+ }
112
+ }
@@ -0,0 +1,2 @@
1
+ import type { Session } from './types.js';
2
+ export declare function refreshIfNeeded(session: Session, apiUrl: string): Promise<Session | null>;
@@ -0,0 +1,29 @@
1
+ import axios from 'axios';
2
+ import { isExpired } from './session.js';
3
+ export async function refreshIfNeeded(session, apiUrl) {
4
+ if (isExpired(session)) {
5
+ return null;
6
+ }
7
+ const now = new Date();
8
+ const minutesUntilExpiry = (session.expiresAt.getTime() - now.getTime()) / (1000 * 60);
9
+ if (minutesUntilExpiry > 5) {
10
+ return session;
11
+ }
12
+ try {
13
+ const response = await axios.post(`${apiUrl}/auth/token/refresh`, {}, {
14
+ headers: { Authorization: `Bearer ${session.token}` },
15
+ });
16
+ return {
17
+ token: response.data.access_token,
18
+ expiresAt: session.expiresAt,
19
+ loginAt: session.loginAt,
20
+ permissions: session.permissions,
21
+ };
22
+ }
23
+ catch (error) {
24
+ if (error.response?.status === 401) {
25
+ return null;
26
+ }
27
+ return session;
28
+ }
29
+ }
@@ -0,0 +1,5 @@
1
+ import type { Session } from './types.js';
2
+ export declare function readSession(): Session | null;
3
+ export declare function writeSession(token: string, expiresIn: number, permissions: string[]): void;
4
+ export declare function deleteSession(): void;
5
+ export declare function isExpired(session: Session): boolean;
@@ -0,0 +1,50 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'mcp-chaos-auth');
5
+ const SESSION_FILE = path.join(CONFIG_DIR, 'session.json');
6
+ export function readSession() {
7
+ try {
8
+ const data = fs.readFileSync(SESSION_FILE, 'utf-8');
9
+ const parsed = JSON.parse(data);
10
+ const session = {
11
+ ...parsed,
12
+ expiresAt: new Date(parsed.expiresAt),
13
+ loginAt: new Date(parsed.loginAt),
14
+ };
15
+ if (isExpired(session)) {
16
+ return null;
17
+ }
18
+ return session;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function writeSession(token, expiresIn, permissions) {
25
+ const now = new Date();
26
+ const session = {
27
+ token,
28
+ expiresAt: new Date(now.getTime() + expiresIn * 1000),
29
+ loginAt: now,
30
+ permissions,
31
+ };
32
+ try {
33
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
34
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2), { mode: 0o600 });
35
+ }
36
+ catch (error) {
37
+ throw new Error(`Failed to write session: ${error}`);
38
+ }
39
+ }
40
+ export function deleteSession() {
41
+ try {
42
+ fs.unlinkSync(SESSION_FILE);
43
+ }
44
+ catch {
45
+ // Ignore errors
46
+ }
47
+ }
48
+ export function isExpired(session) {
49
+ return new Date() > session.expiresAt;
50
+ }
@@ -0,0 +1,14 @@
1
+ export interface Session {
2
+ token: string;
3
+ expiresAt: Date;
4
+ loginAt: Date;
5
+ permissions: string[];
6
+ }
7
+ export interface LoginResponse {
8
+ access_token: string;
9
+ token_type: string;
10
+ expires_in: number;
11
+ permissions?: string[];
12
+ mfa_required?: boolean;
13
+ challenge_token?: string;
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { loginCommand } from './login.js';
2
+ import { statusCommand } from './status.js';
3
+ import { API_URL } from '../config.js';
4
+ const command = process.argv[2];
5
+ if (command === 'login') {
6
+ loginCommand(API_URL);
7
+ }
8
+ else if (command === 'status') {
9
+ statusCommand();
10
+ }
11
+ else {
12
+ console.error('Usage: mcp-chaos-auth <login|status>');
13
+ process.exit(1);
14
+ }
@@ -0,0 +1 @@
1
+ export declare function loginCommand(apiUrl: string): Promise<void>;
@@ -0,0 +1,17 @@
1
+ import { login } from '../auth/login.js';
2
+ import { writeSession } from '../auth/session.js';
3
+ export async function loginCommand(apiUrl) {
4
+ try {
5
+ const session = await login(apiUrl);
6
+ const expiresIn = Math.floor((session.expiresAt.getTime() - session.loginAt.getTime()) / 1000);
7
+ writeSession(session.token, expiresIn, session.permissions);
8
+ console.log('✓ Logged in successfully');
9
+ console.log(`Session valid until: ${session.expiresAt.toLocaleString()}`);
10
+ console.log("Run 'mcp-chaos-auth status' to check session");
11
+ process.exit(0);
12
+ }
13
+ catch (error) {
14
+ console.error(`Error: ${error.message}`);
15
+ process.exit(1);
16
+ }
17
+ }
@@ -0,0 +1 @@
1
+ export declare function statusCommand(): Promise<void>;
@@ -0,0 +1,20 @@
1
+ import { readSession } from '../auth/session.js';
2
+ export async function statusCommand() {
3
+ const session = readSession();
4
+ if (!session) {
5
+ console.log('Not logged in');
6
+ process.exit(0);
7
+ }
8
+ const now = new Date();
9
+ if (now > session.expiresAt) {
10
+ console.log('Session expired');
11
+ process.exit(0);
12
+ }
13
+ const timeRemaining = Math.floor((session.expiresAt.getTime() - now.getTime()) / (1000 * 60));
14
+ const hours = Math.floor(timeRemaining / 60);
15
+ const minutes = timeRemaining % 60;
16
+ console.log(`Session expires: ${session.expiresAt.toLocaleString()}`);
17
+ console.log(`Time remaining: ${hours}h ${minutes}m`);
18
+ console.log(`Permissions: ${session.permissions.length} loaded`);
19
+ process.exit(0);
20
+ }
@@ -0,0 +1 @@
1
+ export declare const API_URL: string;
package/dist/config.js ADDED
@@ -0,0 +1 @@
1
+ export const API_URL = process.env.AUTH_API_URL || 'https://authentication.stable.tastydata.io/api';
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { startServer } from './mcp/server.js';
2
+ import { API_URL } from './config.js';
3
+ startServer(API_URL);
@@ -0,0 +1 @@
1
+ export declare function startServer(apiUrl: string): Promise<void>;
@@ -0,0 +1,71 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { readSession } from '../auth/session.js';
5
+ import { createClient } from '../api/client.js';
6
+ import { validateSession } from '../api/endpoints.js';
7
+ import { tools } from './tools/index.js';
8
+ export async function startServer(apiUrl) {
9
+ const session = readSession();
10
+ if (!session) {
11
+ console.error('No session found. Run: mcp-chaos-auth login');
12
+ process.exit(1);
13
+ }
14
+ const client = createClient(apiUrl);
15
+ try {
16
+ await validateSession(client);
17
+ console.error('MCP server starting');
18
+ console.error(`Session valid until ${session.expiresAt.toISOString()}`);
19
+ console.error(`Loaded ${tools.length} tools`);
20
+ }
21
+ catch (error) {
22
+ console.error(`Cannot reach auth server: ${error.message}`);
23
+ process.exit(1);
24
+ }
25
+ const server = new Server({
26
+ name: 'mcp-chaos-auth',
27
+ version: '1.0.0',
28
+ }, {
29
+ capabilities: {
30
+ tools: {},
31
+ },
32
+ });
33
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
34
+ tools: tools.map((tool) => ({
35
+ name: tool.name,
36
+ description: tool.description,
37
+ inputSchema: tool.inputSchema,
38
+ })),
39
+ }));
40
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
41
+ const tool = tools.find((t) => t.name === request.params.name);
42
+ if (!tool) {
43
+ throw new Error(`Tool not found: ${request.params.name}`);
44
+ }
45
+ try {
46
+ const result = await tool.handler(client);
47
+ return {
48
+ content: [
49
+ {
50
+ type: 'text',
51
+ text: JSON.stringify(result, null, 2),
52
+ },
53
+ ],
54
+ };
55
+ }
56
+ catch (error) {
57
+ return {
58
+ content: [
59
+ {
60
+ type: 'text',
61
+ text: `Error: ${error.message}`,
62
+ },
63
+ ],
64
+ isError: true,
65
+ };
66
+ }
67
+ });
68
+ const transport = new StdioServerTransport();
69
+ await server.connect(transport);
70
+ console.error('MCP server ready');
71
+ }
@@ -0,0 +1,15 @@
1
+ import type { AxiosInstance } from 'axios';
2
+ export declare const exampleTool: {
3
+ name: string;
4
+ description: string;
5
+ inputSchema: {
6
+ type: string;
7
+ properties: {};
8
+ required: never[];
9
+ };
10
+ handler: (client: AxiosInstance) => Promise<{
11
+ username: any;
12
+ roles: any;
13
+ permissions: any;
14
+ }>;
15
+ };
@@ -0,0 +1,17 @@
1
+ export const exampleTool = {
2
+ name: "chaos_auth_whoami",
3
+ description: "Get current authenticated user info",
4
+ inputSchema: {
5
+ type: "object",
6
+ properties: {},
7
+ required: [],
8
+ },
9
+ handler: async (client) => {
10
+ const response = await client.get("/users/me");
11
+ return {
12
+ username: response.data.username,
13
+ roles: response.data.roles,
14
+ permissions: response.data.permissions,
15
+ };
16
+ },
17
+ };
@@ -0,0 +1,14 @@
1
+ export declare const tools: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: string;
6
+ properties: {};
7
+ required: never[];
8
+ };
9
+ handler: (client: import("axios").AxiosInstance) => Promise<{
10
+ username: any;
11
+ roles: any;
12
+ permissions: any;
13
+ }>;
14
+ }[];
@@ -0,0 +1,4 @@
1
+ import { exampleTool } from './example-tool.js';
2
+ export const tools = [
3
+ exampleTool,
4
+ ];
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "dashley-mcp-framework",
3
+ "version": "1.0.1",
4
+ "description": "MCP server with authenticated access to Chaos Auth backend",
5
+ "author": "Joshua Icuss",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/yourusername/mcp-chaos-auth"
10
+ },
11
+ "keywords": ["mcp", "authentication", "chaos-auth"],
12
+ "type": "module",
13
+ "files": [
14
+ "dist",
15
+ "bin"
16
+ ],
17
+ "bin": {
18
+ "tasty-auth": "./bin/tasty-auth",
19
+ "tasty-mcp-server": "./bin/tasty-mcp-server"
20
+ },
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "dev": "tsx src/index.ts",
24
+ "login": "tsx src/cli/index.ts login"
25
+ },
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.0.4",
28
+ "axios": "^1.7.9"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.10.5",
32
+ "tsx": "^4.19.2",
33
+ "typescript": "^5.7.2"
34
+ }
35
+ }