├── .nvmrc ├── demos ├── cursor-mcp.png ├── claude-conf.png ├── posts-webxr.jpeg ├── talks-in-2023.jpeg ├── talks-in-spanish.jpeg ├── videos-about-rag.jpeg └── cursor-videos-in-2024.png ├── Dockerfile ├── test ├── types.ts ├── setup.ts ├── videos.test.ts ├── posts.test.ts └── talks.test.ts ├── mcp.jsonc ├── src ├── utils │ └── language.ts ├── tools │ ├── status.ts │ ├── videos.ts │ ├── posts.ts │ └── talks.ts ├── config │ └── api.ts ├── index.ts ├── types │ └── index.ts └── services │ └── api.ts ├── .github └── workflows │ └── test.yml ├── erickwendel-sdk ├── runtime │ ├── index.ts │ ├── error.ts │ ├── types.ts │ ├── createClient.ts │ ├── fetcher.ts │ ├── typeSelection.ts │ ├── linkTypeMap.ts │ ├── generateGraphqlOperation.ts │ └── batcher.ts ├── index.ts ├── schema.graphql ├── types.ts └── schema.ts ├── package.json ├── LICENSE ├── refs.txt ├── smithery.yaml ├── .gitignore └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | v23.9.0 -------------------------------------------------------------------------------- /demos/cursor-mcp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErickWendel/erickwendel-contributions-mcp/HEAD/demos/cursor-mcp.png -------------------------------------------------------------------------------- /demos/claude-conf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErickWendel/erickwendel-contributions-mcp/HEAD/demos/claude-conf.png -------------------------------------------------------------------------------- /demos/posts-webxr.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErickWendel/erickwendel-contributions-mcp/HEAD/demos/posts-webxr.jpeg -------------------------------------------------------------------------------- /demos/talks-in-2023.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErickWendel/erickwendel-contributions-mcp/HEAD/demos/talks-in-2023.jpeg -------------------------------------------------------------------------------- /demos/talks-in-spanish.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErickWendel/erickwendel-contributions-mcp/HEAD/demos/talks-in-spanish.jpeg -------------------------------------------------------------------------------- /demos/videos-about-rag.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErickWendel/erickwendel-contributions-mcp/HEAD/demos/videos-about-rag.jpeg -------------------------------------------------------------------------------- /demos/cursor-videos-in-2024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ErickWendel/erickwendel-contributions-mcp/HEAD/demos/cursor-videos-in-2024.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:23-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm ci --omit=dev 8 | 9 | COPY . . 10 | 11 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /test/types.ts: -------------------------------------------------------------------------------- 1 | export interface McpToolResponse { 2 | content: Array<{ 3 | type: string; 4 | text: string; 5 | [key: string]: unknown; 6 | }>; 7 | [key: string]: unknown; 8 | } -------------------------------------------------------------------------------- /mcp.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "erickwendel-contributions": { 4 | "command": "npm", 5 | "args": [ 6 | "exec", 7 | "--", 8 | "@smithery/cli@latest", 9 | "run", 10 | "@ErickWendel/erickwendel-contributions-mcp" 11 | ] 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/utils/language.ts: -------------------------------------------------------------------------------- 1 | // Language code mapping 2 | export const LANGUAGE_CODES: Record = { 3 | english: 'en', 4 | spanish: 'es', 5 | portuguese: 'pt-br', 6 | // add more as needed 7 | }; 8 | 9 | /** 10 | * Converts a language name to its corresponding code 11 | * @param language - Language name or code 12 | * @returns The language code or the original input if no mapping found 13 | */ 14 | export function getLanguageCode(language?: string): string | undefined { 15 | if (!language) return undefined; 16 | return LANGUAGE_CODES[language.toLowerCase()] || language; 17 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test MCP Server 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'src/**.ts' 9 | - 'src/**.json' 10 | - 'src/**.yml' 11 | 12 | jobs: 13 | mcp-server-tests: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 23.9.0 24 | 25 | - name: Install dependencies 26 | run: npm ci 27 | 28 | - name: Run tests 29 | run: npm test 30 | -------------------------------------------------------------------------------- /erickwendel-sdk/runtime/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | export { createClient } from './createClient.ts' 3 | export type { ClientOptions } from './createClient.ts' 4 | export type { FieldsSelection } from './typeSelection.ts' 5 | export { generateGraphqlOperation } from './generateGraphqlOperation.ts' 6 | export type { GraphqlOperation } from './generateGraphqlOperation.ts' 7 | export { linkTypeMap } from './linkTypeMap.ts' 8 | // export { Observable } from 'zen-observable-ts' 9 | export { createFetcher } from './fetcher.ts' 10 | export { GenqlError } from './error.ts' 11 | export const everything = { 12 | __scalar: true, 13 | } 14 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js"; 2 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; 3 | 4 | export async function createMcpClient() { 5 | const transport = new StdioClientTransport({ 6 | command: "node", 7 | args: ["src/index.ts"] 8 | }); 9 | 10 | const client = new Client( 11 | { 12 | name: "mcp-test-client", 13 | version: "1.0.0" 14 | }, 15 | { 16 | capabilities: { 17 | prompts: {}, 18 | resources: {}, 19 | tools: {} 20 | } 21 | } 22 | ); 23 | 24 | await client.connect(transport); 25 | return client; 26 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tml-graphql-service", 3 | "version": "1.0.0", 4 | "description": "A service that provides access to TML GraphQL API data", 5 | "main": "src/index.ts", 6 | "type": "module", 7 | "engines": { 8 | "node": ">=20.0.0" 9 | }, 10 | "scripts": { 11 | "start": "node --no-warnings src/index.ts", 12 | "dev": "node --inspect --watch src/index.ts", 13 | "test": "node --no-warnings --test test/**/*.test.ts", 14 | "test:dev": "node --no-warnings --test --watch test/**/*.test.ts" 15 | }, 16 | "dependencies": { 17 | "@modelcontextprotocol/sdk": "^1.5.0", 18 | "zod": "^3.22.4" 19 | }, 20 | "devDependencies": { 21 | "@types/node": "^20.11.24" 22 | }, 23 | "license": "MIT" 24 | } -------------------------------------------------------------------------------- /erickwendel-sdk/runtime/error.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | export class GenqlError extends Error { 3 | errors: Array = [] 4 | /** 5 | * Partial data returned by the server 6 | */ 7 | data?: any 8 | constructor(errors: any[], data: any) { 9 | let message = Array.isArray(errors) 10 | ? errors.map((x) => x?.message || '').join('\n') 11 | : '' 12 | if (!message) { 13 | message = 'GraphQL error' 14 | } 15 | super(message) 16 | this.errors = errors 17 | this.data = data 18 | } 19 | } 20 | 21 | interface GraphqlError { 22 | message: string 23 | locations?: Array<{ 24 | line: number 25 | column: number 26 | }> 27 | path?: string[] 28 | extensions?: Record 29 | } 30 | -------------------------------------------------------------------------------- /src/tools/status.ts: -------------------------------------------------------------------------------- 1 | import type { McpResponse, McpTextContent } from '../types/index.ts'; 2 | import { checkApiStatus } from '../services/api.ts'; 3 | import { TOOL_CONFIG } from '../config/api.ts'; 4 | 5 | /** 6 | * MCP tool definition for checking API status 7 | */ 8 | export const checkStatusTool = { 9 | name: TOOL_CONFIG.status.name, 10 | description: TOOL_CONFIG.status.description, 11 | parameters: {}, 12 | handler: async (): Promise => { 13 | try { 14 | const result = await checkApiStatus(); 15 | 16 | const content: McpTextContent = { 17 | type: "text", 18 | text: `API Status: ${result.isAlive ? "Online" : "Offline"}` 19 | }; 20 | 21 | return { 22 | content: [content], 23 | }; 24 | } catch (error) { 25 | throw new Error(`Failed to check API status: ${error.message}`); 26 | } 27 | } 28 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2025 Erick Wendel 2 | 3 | Permission is hereby granted, free 4 | of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/config/api.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from '../../erickwendel-sdk/index.ts'; 2 | 3 | // GraphQL API endpoint 4 | export const GRAPHQL_API = 'https://tml-api.herokuapp.com/graphql'; 5 | 6 | // Initialize the GraphQL client 7 | export const client = createClient({ 8 | url: GRAPHQL_API, 9 | }); 10 | 11 | // Tool configurations 12 | export const TOOL_CONFIG = { 13 | talks: { 14 | name: "get_talks", 15 | description: "Get a list of talks with optional filtering and pagination." 16 | }, 17 | posts: { 18 | name: "get_posts", 19 | description: "Get a list of posts with optional filtering and pagination." 20 | }, 21 | videos: { 22 | name: "get_videos", 23 | description: "Get a list of videos with optional filtering and pagination." 24 | }, 25 | projects: { 26 | name: "get_projects", 27 | description: "Get a list of projects with optional filtering and pagination." 28 | }, 29 | status: { 30 | name: "check_status", 31 | description: "Check if the API is alive and responding." 32 | } 33 | }; 34 | 35 | // Server configuration 36 | export const SERVER_CONFIG = { 37 | name: "erickwendel-api-service", 38 | version: "1.0.0", 39 | description: "A service that provides access to Erick Wendel's content including talks, posts, videos, and projects.", 40 | }; -------------------------------------------------------------------------------- /src/tools/videos.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import type { VideosParams, McpResponse, McpTextContent } from '../types/index.ts'; 3 | import { fetchVideos } from '../services/api.ts'; 4 | import { TOOL_CONFIG } from '../config/api.ts'; 5 | 6 | /** 7 | * MCP tool definition for getting videos 8 | */ 9 | export const getVideosTool = { 10 | name: TOOL_CONFIG.videos.name, 11 | description: TOOL_CONFIG.videos.description, 12 | parameters: { 13 | id: z.string().optional().describe("Filter videos by ID"), 14 | title: z.string().optional().describe("Filter videos by title"), 15 | language: z.string().optional().describe("Filter videos by language"), 16 | skip: z.number().optional().default(0).describe("Number of videos to skip"), 17 | limit: z.number().optional().default(10).describe("Maximum number of videos to return"), 18 | }, 19 | handler: async (params: VideosParams): Promise => { 20 | try { 21 | const result = await fetchVideos(params); 22 | 23 | if (!result.getVideos) { 24 | throw new Error('No results returned from API'); 25 | } 26 | 27 | const content: McpTextContent = { 28 | type: "text", 29 | text: `Videos Results:\n\n${JSON.stringify(result.getVideos, null, 2)}` 30 | }; 31 | 32 | return { 33 | content: [content], 34 | }; 35 | } catch (error) { 36 | throw new Error(`Failed to fetch videos: ${error.message}`); 37 | } 38 | } 39 | }; -------------------------------------------------------------------------------- /erickwendel-sdk/index.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import type { QueryGenqlSelection, Query } from './schema.ts' 3 | import { 4 | linkTypeMap, 5 | createClient as createClientOriginal, 6 | generateGraphqlOperation, 7 | type FieldsSelection, 8 | type GraphqlOperation, 9 | type ClientOptions, 10 | GenqlError, 11 | } from './runtime/index.ts' 12 | export type { FieldsSelection } from './runtime/index.ts' 13 | export { GenqlError } 14 | 15 | import types from './types.ts' 16 | export * from './schema.ts' 17 | const typeMap = linkTypeMap(types as any) 18 | 19 | export interface Client { 20 | query( 21 | request: R & { __name?: string }, 22 | ): Promise> 23 | } 24 | 25 | export const createClient = function (options?: ClientOptions): Client { 26 | return createClientOriginal({ 27 | url: 'https://tml-api.herokuapp.com/graphql', 28 | 29 | ...options, 30 | queryRoot: typeMap.Query!, 31 | mutationRoot: typeMap.Mutation!, 32 | subscriptionRoot: typeMap.Subscription!, 33 | }) as any 34 | } 35 | 36 | export const everything = { 37 | __scalar: true, 38 | } 39 | 40 | export type QueryResult = FieldsSelection< 41 | Query, 42 | fields 43 | > 44 | export const generateQueryOp: ( 45 | fields: QueryGenqlSelection & { __name?: string }, 46 | ) => GraphqlOperation = function (fields) { 47 | return generateGraphqlOperation('query', typeMap.Query!, fields as any) 48 | } 49 | -------------------------------------------------------------------------------- /src/tools/posts.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import type { PostsParams, McpResponse, McpTextContent } from '../types/index.ts'; 3 | import { fetchPosts } from '../services/api.ts'; 4 | import { TOOL_CONFIG } from '../config/api.ts'; 5 | 6 | /** 7 | * MCP tool definition for getting posts 8 | */ 9 | export const getPostsTool = { 10 | name: TOOL_CONFIG.posts.name, 11 | description: TOOL_CONFIG.posts.description, 12 | parameters: { 13 | id: z.string().optional().describe("Filter posts by ID"), 14 | title: z.string().optional().describe("Filter posts by title"), 15 | language: z.string().optional().describe("Filter posts by language"), 16 | portal: z.string().optional().describe("Filter posts by portal"), 17 | skip: z.number().optional().default(0).describe("Number of posts to skip"), 18 | limit: z.number().optional().default(10).describe("Maximum number of posts to return"), 19 | }, 20 | handler: async (params: PostsParams): Promise => { 21 | try { 22 | const result = await fetchPosts(params); 23 | 24 | if (!result.getPosts) { 25 | throw new Error('No results returned from API'); 26 | } 27 | 28 | const content: McpTextContent = { 29 | type: "text", 30 | text: `Posts Results:\n\n${JSON.stringify(result.getPosts, null, 2)}` 31 | }; 32 | 33 | return { 34 | content: [content], 35 | }; 36 | } catch (error) { 37 | throw new Error(`Failed to fetch posts: ${error.message}`); 38 | } 39 | } 40 | }; -------------------------------------------------------------------------------- /test/videos.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, after } from 'node:test'; 2 | import assert from 'node:assert'; 3 | import { createMcpClient } from './setup.ts'; 4 | import { TOOL_CONFIG } from '../src/config/api.ts'; 5 | import type { McpToolResponse } from './types.ts'; 6 | 7 | describe('Videos API Tests', async () => { 8 | const client = await createMcpClient(); 9 | after(async () => { 10 | await client.close(); 11 | }); 12 | it('should get a list of videos with default pagination', async () => { 13 | const result = await client.callTool({ 14 | name: TOOL_CONFIG.videos.name, 15 | arguments: {} 16 | }) as McpToolResponse; 17 | 18 | assert.ok(result.content[0].text.includes('Videos Results')); 19 | const data = JSON.parse(result.content[0].text.split('\n\n')[1]); 20 | assert.ok(data.totalCount > 0); 21 | assert.ok(Array.isArray(data.videos)); 22 | assert.equal(data.videos.length, 10); // Default limit 23 | }); 24 | 25 | it('should get a limited number of videos', async () => { 26 | const limit = 2; 27 | const result = await client.callTool({ 28 | name: TOOL_CONFIG.videos.name, 29 | arguments: { limit } 30 | }) as McpToolResponse; 31 | 32 | const data = JSON.parse(result.content[0].text.split('\n\n')[1]); 33 | assert.equal(data.videos.length, limit); 34 | }); 35 | 36 | it('should filter videos by language', async () => { 37 | const result = await client.callTool({ 38 | name: TOOL_CONFIG.videos.name, 39 | arguments: { 40 | language: 'en-us', 41 | limit: 5 42 | } 43 | }) as McpToolResponse; 44 | 45 | const data = JSON.parse(result.content[0].text.split('\n\n')[1]); 46 | assert.ok(data.videos.every(video => video.language === 'en-us')); 47 | }); 48 | }); -------------------------------------------------------------------------------- /erickwendel-sdk/runtime/types.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | export interface ExecutionResult { 4 | errors?: Array 5 | data?: TData | null 6 | } 7 | 8 | export interface ArgMap { 9 | [arg: string]: [keyType, string] | [keyType] | undefined 10 | } 11 | 12 | export type CompressedField = [ 13 | type: keyType, 14 | args?: ArgMap, 15 | ] 16 | 17 | export interface CompressedFieldMap { 18 | [field: string]: CompressedField | undefined 19 | } 20 | 21 | export type CompressedType = CompressedFieldMap 22 | 23 | export interface CompressedTypeMap { 24 | scalars: Array 25 | types: { 26 | [type: string]: CompressedType | undefined 27 | } 28 | } 29 | 30 | // normal types 31 | export type Field = { 32 | type: keyType 33 | args?: ArgMap 34 | } 35 | 36 | export interface FieldMap { 37 | [field: string]: Field | undefined 38 | } 39 | 40 | export type Type = FieldMap 41 | 42 | export interface TypeMap { 43 | scalars: Array 44 | types: { 45 | [type: string]: Type | undefined 46 | } 47 | } 48 | 49 | export interface LinkedArgMap { 50 | [arg: string]: [LinkedType, string] | undefined 51 | } 52 | export interface LinkedField { 53 | type: LinkedType 54 | args?: LinkedArgMap 55 | } 56 | 57 | export interface LinkedFieldMap { 58 | [field: string]: LinkedField | undefined 59 | } 60 | 61 | export interface LinkedType { 62 | name: string 63 | fields?: LinkedFieldMap 64 | scalar?: string[] 65 | } 66 | 67 | export interface LinkedTypeMap { 68 | [type: string]: LinkedType | undefined 69 | } 70 | -------------------------------------------------------------------------------- /erickwendel-sdk/runtime/createClient.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import { type BatchOptions, createFetcher } from './fetcher.ts' 4 | import type { ExecutionResult, LinkedType } from './types.ts' 5 | import { 6 | generateGraphqlOperation, 7 | type GraphqlOperation, 8 | } from './generateGraphqlOperation.ts' 9 | 10 | export type Headers = 11 | | HeadersInit 12 | | (() => HeadersInit) 13 | | (() => Promise) 14 | 15 | export type BaseFetcher = ( 16 | operation: GraphqlOperation | GraphqlOperation[], 17 | ) => Promise 18 | 19 | export type ClientOptions = Omit & { 20 | url?: string 21 | batch?: BatchOptions | boolean 22 | fetcher?: BaseFetcher 23 | fetch?: Function 24 | headers?: Headers 25 | } 26 | 27 | export const createClient = ({ 28 | queryRoot, 29 | mutationRoot, 30 | subscriptionRoot, 31 | ...options 32 | }: ClientOptions & { 33 | queryRoot?: LinkedType 34 | mutationRoot?: LinkedType 35 | subscriptionRoot?: LinkedType 36 | }) => { 37 | const fetcher = createFetcher(options) 38 | const client: { 39 | query?: Function 40 | mutation?: Function 41 | } = {} 42 | 43 | if (queryRoot) { 44 | client.query = (request: any) => { 45 | if (!queryRoot) throw new Error('queryRoot argument is missing') 46 | 47 | const resultPromise = fetcher( 48 | generateGraphqlOperation('query', queryRoot, request), 49 | ) 50 | 51 | return resultPromise 52 | } 53 | } 54 | if (mutationRoot) { 55 | client.mutation = (request: any) => { 56 | if (!mutationRoot) 57 | throw new Error('mutationRoot argument is missing') 58 | 59 | const resultPromise = fetcher( 60 | generateGraphqlOperation('mutation', mutationRoot, request), 61 | ) 62 | 63 | return resultPromise 64 | } 65 | } 66 | 67 | return client as any 68 | } 69 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { SERVER_CONFIG } from './config/api.ts'; 4 | import { getTalksTool } from './tools/talks.ts'; 5 | import { getPostsTool } from './tools/posts.ts'; 6 | import { getVideosTool } from './tools/videos.ts'; 7 | import { checkStatusTool } from './tools/status.ts'; 8 | 9 | /** 10 | * Initialize the MCP server and register all tools 11 | */ 12 | async function initializeServer() { 13 | // Create server instance 14 | const server = new McpServer({ 15 | name: SERVER_CONFIG.name, 16 | version: SERVER_CONFIG.version, 17 | description: SERVER_CONFIG.description, 18 | }); 19 | 20 | // Register all tools 21 | server.tool( 22 | getTalksTool.name, 23 | getTalksTool.description, 24 | getTalksTool.parameters, 25 | getTalksTool.handler 26 | ); 27 | 28 | server.tool( 29 | getPostsTool.name, 30 | getPostsTool.description, 31 | getPostsTool.parameters, 32 | getPostsTool.handler 33 | ); 34 | 35 | server.tool( 36 | getVideosTool.name, 37 | getVideosTool.description, 38 | getVideosTool.parameters, 39 | getVideosTool.handler 40 | ); 41 | 42 | server.tool( 43 | checkStatusTool.name, 44 | checkStatusTool.description, 45 | checkStatusTool.parameters, 46 | checkStatusTool.handler 47 | ); 48 | 49 | return server; 50 | } 51 | 52 | /** 53 | * Main entry point 54 | */ 55 | async function main() { 56 | // Initialize the server 57 | const server = await initializeServer(); 58 | 59 | // Connect to stdio transport 60 | const transport = new StdioServerTransport(); 61 | await server.connect(transport); 62 | // console.error for claude-desktop so it won't process this output 63 | console.error("Erick Wendel API MCP Server running on stdio"); 64 | } 65 | 66 | // Start the server 67 | main().catch((error) => { 68 | console.error("Fatal error in main():", error); 69 | process.exit(1); 70 | }); 71 | -------------------------------------------------------------------------------- /test/posts.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, after } from 'node:test'; 2 | import assert from 'node:assert'; 3 | import { createMcpClient } from './setup.ts'; 4 | import { TOOL_CONFIG } from '../src/config/api.ts'; 5 | import type { McpToolResponse } from './types.ts'; 6 | 7 | describe('Posts API Tests', async () => { 8 | const client = await createMcpClient(); 9 | after(async () => { 10 | await client.close(); 11 | }); 12 | 13 | it('should get a list of posts with default pagination', async () => { 14 | const result = await client.callTool({ 15 | name: TOOL_CONFIG.posts.name, 16 | arguments: {} 17 | }) as McpToolResponse; 18 | 19 | assert.ok(result.content[0].text.includes('Posts Results')); 20 | const data = JSON.parse(result.content[0].text.split('\n\n')[1]); 21 | assert.ok(data.totalCount > 0); 22 | assert.ok(Array.isArray(data.posts)); 23 | assert.equal(data.posts.length, 10); // Default limit 24 | }); 25 | 26 | it('should get a limited number of posts', async () => { 27 | const limit = 2; 28 | const result = await client.callTool({ 29 | name: TOOL_CONFIG.posts.name, 30 | arguments: { limit } 31 | }) as McpToolResponse; 32 | 33 | const data = JSON.parse(result.content[0].text.split('\n\n')[1]); 34 | assert.equal(data.posts.length, limit); 35 | }); 36 | 37 | it('should filter posts by language', async () => { 38 | const result = await client.callTool({ 39 | name: TOOL_CONFIG.posts.name, 40 | arguments: { 41 | language: 'en-us', 42 | limit: 5 43 | } 44 | }) as McpToolResponse; 45 | 46 | const data = JSON.parse(result.content[0].text.split('\n\n')[1]); 47 | assert.ok(data.posts.every(post => post.language === 'en-us')); 48 | }); 49 | 50 | it('should filter posts by portal', async () => { 51 | const portal = 'Linkedin'; 52 | const result = await client.callTool({ 53 | name: TOOL_CONFIG.posts.name, 54 | arguments: { 55 | portal, 56 | limit: 5 57 | } 58 | }) as McpToolResponse; 59 | 60 | const data = JSON.parse(result.content[0].text.split('\n\n')[1]); 61 | assert.ok(data.posts.every(post => post.portal?.name?.includes(portal))); 62 | }); 63 | }); -------------------------------------------------------------------------------- /refs.txt: -------------------------------------------------------------------------------- 1 | https://genql.dev/docs 2 | https://github.com/modelcontextprotocol/typescript-sdk 3 | const GRAPHQL_API = 'https://tml-api.herokuapp.com/graphql'; 4 | https://smithery.ai//?q=webui 5 | https://github.com/NightTrek/Ollama-mcp/blob/main/src/index.ts 6 | https://smithery.ai/server/@smithery-ai/brave-search 7 | https://github.com/punkpeye/awesome-mcp-clients 8 | https://github.com/open-webui/open-webui/discussions/7431 9 | https://github.com/dotansimha/graphql-code-generator 10 | https://www.aihero.dev/mcp-server-from-a-single-typescript-file?list=model-context-protocol-tutorial 11 | https://modelcontextprotocol.io/introduction 12 | https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview 13 | 14 | https://github.com/patruff/ollama-mcp-bridge 15 | https://erickwendel.com.br/ 16 | https://tml-api.herokuapp.com/graphiql 17 | https://neo4j.com/blog/developer/claude-converses-neo4j-via-mcp/ 18 | https://github.com/open-webui/open-webui/discussions/7363 19 | https://github.com/resend/mcp-send-email/tree/main 20 | https://docs.cursor.com/settings/models 21 | https://github.com/modelcontextprotocol/inspector 22 | npx @modelcontextprotocol/inspector npm start 23 | https://smithery.ai/docs/config 24 | https://github.com/modelcontextprotocol/servers 25 | { 26 | "mcpServers": { 27 | "erick-wendel-api": { 28 | "command": "/Users/erickwendel/.nvm/versions/node/v23.9.0/bin/node", 29 | "args": ["/Users/erickwendel/Downloads/projetos/mcp-aai-example/src/index.ts"] 30 | } 31 | } 32 | } 33 | 34 | 35 | 36 | go env GOROOT GOPATH 37 | export GOROOT=$(brew --prefix go)/libexec 38 | export PATH=$GOROOT/bin:$PATH 39 | export GOPATH=$HOME/go 40 | export PATH=$GOPATH/bin:$PATH 41 | 42 | 43 | echo 'export GOROOT=$(brew --prefix go)/libexec' >> ~/.zshrc 44 | echo 'export PATH=$GOROOT/bin:$PATH' >> ~/.zshrc 45 | echo 'export GOPATH=$HOME/go' >> ~/.zshrc 46 | echo 'export PATH=$GOPATH/bin:$PATH' >> ~/.zshrc 47 | source ~/.zshrc # Apply changes 48 | 49 | go version 50 | go env GOROOT GOPATH 51 | 52 | go install github.com/mark3labs/mcphost@latest 53 | mcphost --config ./mcp.json -m ollama:qwen2.5-coder:7b-instruct 54 | mcphost --config ./mcp.json -m ollama:deepseek-coder:6.7b 55 | mcphost --config ./mcp.json -m ollama:qwen2.5:3b 56 | 57 | 58 | npm exec -- @smithery/cli@latest run @ErickWendel/erickwendel-contributions-mcp 59 | -------------------------------------------------------------------------------- /erickwendel-sdk/schema.graphql: -------------------------------------------------------------------------------- 1 | type Query { 2 | getTalks(_id: ID, title: String, language: String, city: String, country: String, skip: Int = 0, limit: Int = 10): TalkQuery 3 | isAlive: Boolean 4 | getPosts(_id: ID, title: String, language: String, portal: String, skip: Int = 0, limit: Int = 10): PostQuery 5 | getVideos(_id: ID, title: String, language: String, skip: Int = 0, limit: Int = 10): VideoQuery 6 | getProjects(_id: ID, title: String, language: String, skip: Int = 0, limit: Int = 10): ProjectQuery 7 | } 8 | 9 | type TalkQuery { 10 | totalCount: Int! 11 | retrieved: Int! 12 | processedIn: Int! 13 | talks: [Talk] 14 | } 15 | 16 | type Talk { 17 | _id: ID 18 | title: String 19 | abstract: String 20 | type: String 21 | event: Event 22 | slides: String 23 | images: [Image] 24 | video: String 25 | tags: [String] 26 | location: Location 27 | additionalLinks: [String] 28 | language: String 29 | date: String 30 | } 31 | 32 | type Event { 33 | link: String 34 | name: String 35 | } 36 | 37 | type Image { 38 | url: String 39 | filename: String 40 | size: String 41 | pathId: String 42 | } 43 | 44 | type Location { 45 | latitude: Float 46 | longitude: Float 47 | country: String 48 | uf: String 49 | city: String 50 | } 51 | 52 | type PostQuery { 53 | totalCount: Int! 54 | retrieved: Int! 55 | processedIn: Int! 56 | posts: [Post] 57 | } 58 | 59 | type Post { 60 | _id: ID 61 | title: String 62 | abstract: String 63 | type: String 64 | link: String 65 | additionalLinks: [String] 66 | portal: Portal 67 | tags: [String] 68 | language: String 69 | date: String 70 | } 71 | 72 | type Portal { 73 | link: String 74 | name: String 75 | } 76 | 77 | type VideoQuery { 78 | totalCount: Int! 79 | retrieved: Int! 80 | processedIn: Int! 81 | videos: [Video] 82 | } 83 | 84 | type Video { 85 | _id: ID 86 | title: String 87 | abstract: String 88 | type: String 89 | link: String 90 | additionalLinks: [String] 91 | tags: [String] 92 | language: String 93 | date: String 94 | } 95 | 96 | type ProjectQuery { 97 | totalCount: Int! 98 | retrieved: Int! 99 | processedIn: Int! 100 | projects: [Project] 101 | } 102 | 103 | type Project { 104 | _id: ID 105 | title: String 106 | abstract: String 107 | type: String 108 | link: String 109 | additionalLinks: [String] 110 | tags: [String] 111 | language: String 112 | date: String 113 | } -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | name: erickwendel-contributions-mcp 2 | description: A Model Context Protocol (MCP) server that provides tools to query Erick Wendel's contributions across different platforms. 3 | version: 1.0.0 4 | author: Erick Wendel 5 | repository: https://github.com/ErickWendel/erickwendel-contributions-mcp 6 | license: MIT 7 | 8 | # Smithery.ai configuration 9 | startCommand: 10 | type: stdio 11 | configSchema: 12 | # JSON Schema defining the configuration options for the MCP. 13 | {} 14 | commandFunction: 15 | # A function that produces the CLI command to start the MCP on stdio. 16 | |- 17 | (config) => ({ 18 | "command": "node", 19 | "args": [ 20 | "--no-warnings", 21 | "src/index.ts" 22 | ] 23 | }) 24 | 25 | tools: 26 | - name: get-talks 27 | description: Retrieves a paginated list of talks with optional filtering 28 | parameters: 29 | - name: language 30 | type: string 31 | description: Filter talks by language (e.g., 'spanish', 'english', 'portuguese') 32 | optional: true 33 | - name: country 34 | type: string 35 | description: Filter talks by country 36 | optional: true 37 | - name: city 38 | type: string 39 | description: Filter talks by city 40 | optional: true 41 | - name: year 42 | type: number 43 | description: Filter talks by year 44 | optional: true 45 | - name: title 46 | type: string 47 | description: Filter talks by title 48 | optional: true 49 | 50 | - name: get-posts 51 | description: Fetches posts with optional filtering and pagination 52 | parameters: 53 | - name: language 54 | type: string 55 | description: Filter posts by language 56 | optional: true 57 | - name: title 58 | type: string 59 | description: Filter posts by title 60 | optional: true 61 | - name: portal 62 | type: string 63 | description: Filter posts by portal 64 | optional: true 65 | 66 | - name: get-videos 67 | description: Retrieves videos with optional filtering and pagination 68 | parameters: 69 | - name: language 70 | type: string 71 | description: Filter videos by language 72 | optional: true 73 | - name: title 74 | type: string 75 | description: Filter videos by title 76 | optional: true 77 | 78 | - name: check-status 79 | description: Verifies if the API is alive and responding -------------------------------------------------------------------------------- /test/talks.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, after } from 'node:test'; 2 | import assert from 'node:assert'; 3 | import { createMcpClient } from './setup.ts'; 4 | import { TOOL_CONFIG } from '../src/config/api.ts'; 5 | import type { McpToolResponse } from './types.ts'; 6 | 7 | describe('Talks API Tests', async () => { 8 | const client = await createMcpClient(); 9 | after(async () => { 10 | await client.close(); 11 | }); 12 | it('should get a list of talks with default pagination', async () => { 13 | const result = await client.callTool({ 14 | name: TOOL_CONFIG.talks.name, 15 | arguments: {} 16 | }) as McpToolResponse; 17 | 18 | assert.ok(result.content[0].text.includes('Talks Results')); 19 | const data = JSON.parse(result.content[0].text.split('\n\n')[1]); 20 | assert.ok(data.totalCount > 0); 21 | assert.ok(Array.isArray(data.talks)); 22 | assert.equal(data.talks.length, 10); // Default limit 23 | }); 24 | 25 | it('should get a limited number of talks', async () => { 26 | const limit = 2; 27 | const result = await client.callTool({ 28 | name: TOOL_CONFIG.talks.name, 29 | arguments: { limit } 30 | }) as McpToolResponse; 31 | 32 | const data = JSON.parse(result.content[0].text.split('\n\n')[1]); 33 | assert.equal(data.talks.length, limit); 34 | }); 35 | 36 | it('should filter talks by language', async () => { 37 | const result = await client.callTool({ 38 | name: TOOL_CONFIG.talks.name, 39 | arguments: { 40 | language: 'es', 41 | limit: 5 42 | } 43 | }) as McpToolResponse; 44 | 45 | const data = JSON.parse(result.content[0].text.split('\n\n')[1]); 46 | assert.ok(data.talks.every(talk => talk.language === 'es')); 47 | }); 48 | 49 | // it('should filter talks by country', async () => { 50 | // const country = 'Spain'; 51 | // const result = await client.callTool({ 52 | // name: TOOL_CONFIG.talks.name, 53 | // arguments: { 54 | // country, 55 | // limit: 5 56 | // } 57 | // }) as McpToolResponse; 58 | // console.log('content', result.content); 59 | // const data = JSON.parse(result.content[0].text.split('\n\n')[1]); 60 | // assert.ok(data.talks.every(talk => talk.location?.country === country)); 61 | // }); 62 | 63 | // it('should get talk counts by group', async () => { 64 | // const result = await client.callTool({ 65 | // name: TOOL_CONFIG.talks.name, 66 | // arguments: { 67 | // count_only: true, 68 | // group_by: 'language' 69 | // } 70 | // }) as McpToolResponse; 71 | 72 | // assert.ok(result.content[0].text.includes('Total talks:')); 73 | // assert.ok(result.content[0].text.includes('Breakdown by language:')); 74 | // }); 75 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | -------------------------------------------------------------------------------- /erickwendel-sdk/runtime/fetcher.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { QueryBatcher } from './batcher.ts' 3 | 4 | import type { ClientOptions } from './createClient.ts' 5 | import type { GraphqlOperation } from './generateGraphqlOperation.ts' 6 | import { GenqlError } from './error.ts' 7 | 8 | export interface Fetcher { 9 | (gql: GraphqlOperation): Promise 10 | } 11 | 12 | export type BatchOptions = { 13 | batchInterval?: number // ms 14 | maxBatchSize?: number 15 | } 16 | 17 | const DEFAULT_BATCH_OPTIONS = { 18 | maxBatchSize: 10, 19 | batchInterval: 40, 20 | } 21 | 22 | export const createFetcher = ({ 23 | url, 24 | headers = {}, 25 | fetcher, 26 | fetch: _fetch, 27 | batch = false, 28 | ...rest 29 | }: ClientOptions): Fetcher => { 30 | if (!url && !fetcher) { 31 | throw new Error('url or fetcher is required') 32 | } 33 | 34 | fetcher = fetcher || (async (body) => { 35 | let headersObject = 36 | typeof headers == 'function' ? await headers() : headers 37 | headersObject = headersObject || {} 38 | if (typeof fetch === 'undefined' && !_fetch) { 39 | throw new Error( 40 | 'Global `fetch` function is not available, pass a fetch polyfill to Genql `createClient`', 41 | ) 42 | } 43 | let fetchImpl = _fetch || fetch 44 | const res = await fetchImpl(url!, { 45 | headers: { 46 | 'Content-Type': 'application/json', 47 | ...headersObject, 48 | }, 49 | method: 'POST', 50 | body: JSON.stringify(body), 51 | ...rest, 52 | }) 53 | if (!res.ok) { 54 | throw new Error(`${res.statusText}: ${await res.text()}`) 55 | } 56 | const json = await res.json() 57 | return json 58 | }) 59 | 60 | if (!batch) { 61 | return async (body) => { 62 | const json = await fetcher!(body) 63 | if (Array.isArray(json)) { 64 | return json.map((json) => { 65 | if (json?.errors?.length) { 66 | throw new GenqlError(json.errors || [], json.data) 67 | } 68 | return json.data 69 | }) 70 | } else { 71 | if (json?.errors?.length) { 72 | throw new GenqlError(json.errors || [], json.data) 73 | } 74 | return json.data 75 | } 76 | } 77 | } 78 | 79 | const batcher = new QueryBatcher( 80 | async (batchedQuery) => { 81 | // console.log(batchedQuery) // [{ query: 'query{user{age}}', variables: {} }, ...] 82 | const json = await fetcher!(batchedQuery) 83 | return json as any 84 | }, 85 | batch === true ? DEFAULT_BATCH_OPTIONS : batch, 86 | ) 87 | 88 | return async ({ query, variables }) => { 89 | const json = await batcher.fetch(query, variables) 90 | if (json?.data) { 91 | return json.data 92 | } 93 | throw new Error( 94 | 'Genql batch fetcher returned unexpected result ' + JSON.stringify(json), 95 | ) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /erickwendel-sdk/runtime/typeSelection.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | ////////////////////////////////////////////////// 3 | 4 | // SOME THINGS TO KNOW BEFORE DIVING IN 5 | /* 6 | 0. DST is the request type, SRC is the response type 7 | 8 | 1. FieldsSelection uses an object because currently is impossible to make recursive types 9 | 10 | 2. FieldsSelection is a recursive type that makes a type based on request type and fields 11 | 12 | 3. HandleObject handles object types 13 | 14 | 4. Handle__scalar adds all scalar properties excluding non scalar props 15 | */ 16 | 17 | export type FieldsSelection | undefined, DST> = { 18 | scalar: SRC 19 | union: Handle__isUnion 20 | object: HandleObject 21 | array: SRC extends Nil 22 | ? never 23 | : SRC extends Array 24 | ? Array> 25 | : never 26 | __scalar: Handle__scalar 27 | never: never 28 | }[DST extends Nil 29 | ? 'never' 30 | : DST extends false | 0 31 | ? 'never' 32 | : SRC extends Scalar 33 | ? 'scalar' 34 | : SRC extends any[] 35 | ? 'array' 36 | : SRC extends { __isUnion?: any } 37 | ? 'union' 38 | : DST extends { __scalar?: any } 39 | ? '__scalar' 40 | : DST extends {} 41 | ? 'object' 42 | : 'never'] 43 | 44 | type HandleObject, DST> = DST extends boolean 45 | ? SRC 46 | : SRC extends Nil 47 | ? never 48 | : Pick< 49 | { 50 | // using keyof SRC to maintain ?: relations of SRC type 51 | [Key in keyof SRC]: Key extends keyof DST 52 | ? FieldsSelection> 53 | : SRC[Key] 54 | }, 55 | Exclude 56 | // { 57 | // // remove falsy values 58 | // [Key in keyof DST]: DST[Key] extends false | 0 ? never : Key 59 | // }[keyof DST] 60 | > 61 | 62 | type Handle__scalar, DST> = SRC extends Nil 63 | ? never 64 | : Pick< 65 | // continue processing fields that are in DST, directly pass SRC type if not in DST 66 | { 67 | [Key in keyof SRC]: Key extends keyof DST 68 | ? FieldsSelection 69 | : SRC[Key] 70 | }, 71 | // remove fields that are not scalars or are not in DST 72 | { 73 | [Key in keyof SRC]: SRC[Key] extends Nil 74 | ? never 75 | : Key extends FieldsToRemove 76 | ? never 77 | : SRC[Key] extends Scalar 78 | ? Key 79 | : Key extends keyof DST 80 | ? Key 81 | : never 82 | }[keyof SRC] 83 | > 84 | 85 | type Handle__isUnion, DST> = SRC extends Nil 86 | ? never 87 | : Omit // just return the union type 88 | 89 | type Scalar = string | number | Date | boolean | null | undefined 90 | 91 | type Anify = { [P in keyof T]?: any } 92 | 93 | type FieldsToRemove = '__isUnion' | '__scalar' | '__name' | '__args' 94 | 95 | type Nil = undefined | null 96 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | // Response types for GraphQL queries 2 | 3 | export interface Location { 4 | country: string | null; 5 | city: string | null; 6 | } 7 | 8 | export interface Event { 9 | link: string | null; 10 | name: string | null; 11 | } 12 | 13 | export interface Talk { 14 | _id: string; 15 | title: string; 16 | abstract: string | null; 17 | type: string | null; 18 | event: Event | null; 19 | slides: string | null; 20 | video: string | null; 21 | tags: string[]; 22 | location: Location | null; 23 | language: string; 24 | date: string; 25 | } 26 | 27 | export interface TalksResponse { 28 | getTalks: { 29 | totalCount: number; 30 | retrieved: number; 31 | processedIn: number; 32 | talks: Talk[]; 33 | } | null; 34 | } 35 | 36 | export interface Post { 37 | _id: string; 38 | title: string; 39 | abstract: string | null; 40 | type: string | null; 41 | link: string | null; 42 | additionalLinks: string[]; 43 | portal: { 44 | link: string | null; 45 | name: string | null; 46 | } | null; 47 | tags: string[]; 48 | language: string; 49 | date: string; 50 | } 51 | 52 | export interface PostsResponse { 53 | getPosts: { 54 | totalCount: number; 55 | retrieved: number; 56 | processedIn: number; 57 | posts: Post[]; 58 | } | null; 59 | } 60 | 61 | export interface Video { 62 | _id: string; 63 | title: string; 64 | abstract: string | null; 65 | type: string | null; 66 | link: string | null; 67 | additionalLinks: string[]; 68 | tags: string[]; 69 | language: string; 70 | date: string; 71 | } 72 | 73 | export interface VideosResponse { 74 | getVideos: { 75 | totalCount: number; 76 | retrieved: number; 77 | processedIn: number; 78 | videos: Video[]; 79 | } | null; 80 | } 81 | 82 | 83 | export interface StatusResponse { 84 | isAlive: boolean; 85 | } 86 | 87 | // Tool parameters types 88 | export interface TalksParams { 89 | id?: string; 90 | title?: string; 91 | language?: string; 92 | city?: string; 93 | country?: string; 94 | year?: number; 95 | skip?: number; 96 | limit?: number; 97 | count_only?: boolean; 98 | group_by?: string; 99 | } 100 | 101 | export interface PostsParams { 102 | id?: string; 103 | title?: string; 104 | language?: string; 105 | portal?: string; 106 | skip?: number; 107 | limit?: number; 108 | } 109 | 110 | export interface VideosParams { 111 | id?: string; 112 | title?: string; 113 | language?: string; 114 | skip?: number; 115 | limit?: number; 116 | } 117 | 118 | 119 | // MCP response types 120 | export interface McpTextContent { 121 | type: "text"; 122 | text: string; 123 | [key: string]: unknown; 124 | } 125 | 126 | export interface McpImageContent { 127 | type: "image"; 128 | data: string; 129 | mimeType: string; 130 | [key: string]: unknown; 131 | } 132 | 133 | export interface McpResourceContent { 134 | type: "resource"; 135 | resource: { 136 | text: string; 137 | uri: string; 138 | mimeType?: string; 139 | [key: string]: unknown; 140 | } | { 141 | uri: string; 142 | blob: string; 143 | mimeType?: string; 144 | [key: string]: unknown; 145 | }; 146 | [key: string]: unknown; 147 | } 148 | 149 | export type McpContent = McpTextContent | McpImageContent | McpResourceContent; 150 | 151 | export interface McpResponse { 152 | content: McpContent[]; 153 | _meta?: Record; 154 | isError?: boolean; 155 | [key: string]: unknown; 156 | } -------------------------------------------------------------------------------- /src/tools/talks.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import type { TalksParams, Talk, McpResponse, McpTextContent } from '../types/index.ts'; 3 | import { fetchTalks, fetchTalksByYear } from '../services/api.ts'; 4 | import { TOOL_CONFIG } from '../config/api.ts'; 5 | 6 | /** 7 | * Calculates group counts from an array of talks 8 | */ 9 | function calculateGroupCounts(talks: Talk[], groupBy: string): Map { 10 | const counts = new Map(); 11 | 12 | talks.forEach(talk => { 13 | if (!talk) return; 14 | 15 | let value = ''; 16 | switch(groupBy) { 17 | case 'language': 18 | value = talk.language || 'unknown'; 19 | break; 20 | case 'country': 21 | value = talk.location?.country || 'unknown'; 22 | break; 23 | case 'city': 24 | value = talk.location?.city || 'unknown'; 25 | break; 26 | default: 27 | return; 28 | } 29 | counts.set(value, (counts.get(value) || 0) + 1); 30 | }); 31 | 32 | return counts; 33 | } 34 | 35 | /** 36 | * Formats the group counts for display 37 | */ 38 | function formatGroupCounts(counts: Map, groupBy: string): string { 39 | let response = `\n\nBreakdown by ${groupBy}:`; 40 | for (const [key, count] of counts.entries()) { 41 | response += `\n${key}: ${count}`; 42 | } 43 | return response; 44 | } 45 | 46 | /** 47 | * MCP tool definition for getting talks 48 | */ 49 | export const getTalksTool = { 50 | name: TOOL_CONFIG.talks.name, 51 | description: TOOL_CONFIG.talks.description, 52 | parameters: { 53 | id: z.string().optional().describe("Filter talks by ID"), 54 | title: z.string().optional().describe("Filter talks by title"), 55 | language: z.string().optional().describe("Filter talks by language (e.g., 'spanish', 'english', 'portuguese' or direct codes like 'es', 'en', 'pt-br')"), 56 | city: z.string().optional().describe("Filter talks by city"), 57 | country: z.string().optional().describe("Filter talks by country"), 58 | year: z.number().optional().describe("Filter talks by year"), 59 | skip: z.number().optional().default(0).describe("Number of talks to skip"), 60 | limit: z.number().optional().default(10).describe("Maximum number of talks to return"), 61 | count_only: z.boolean().optional().default(false).describe("If true, returns only the count without talk details"), 62 | group_by: z.string().optional().describe("Group counts by a specific field (language, country, city)"), 63 | }, 64 | handler: async (params: TalksParams): Promise => { 65 | try { 66 | const { id, title, language, city, country, year, skip, limit, count_only, group_by } = params; 67 | 68 | // Handle year-specific filtering 69 | if (year) { 70 | const allTalks = await fetchTalksByYear({ id, title, language, city, country, year }); 71 | 72 | if (count_only) { 73 | let response = `Total talks in ${year}: ${allTalks.length}`; 74 | 75 | if (group_by) { 76 | const counts = calculateGroupCounts(allTalks, group_by); 77 | response += formatGroupCounts(counts, group_by); 78 | } 79 | 80 | const content: McpTextContent = { 81 | type: "text", 82 | text: response 83 | }; 84 | 85 | return { 86 | content: [content], 87 | }; 88 | } 89 | 90 | // Apply pagination to filtered results 91 | const paginatedTalks = allTalks.slice(skip || 0, (skip || 0) + (limit || 10)); 92 | 93 | const content: McpTextContent = { 94 | type: "text", 95 | text: `Talks Results for ${year}:\n\n${JSON.stringify({ 96 | totalCount: allTalks.length, 97 | retrieved: paginatedTalks.length, 98 | talks: paginatedTalks 99 | }, null, 2)}` 100 | }; 101 | 102 | return { 103 | content: [content], 104 | }; 105 | } 106 | 107 | // Regular query without year filtering 108 | const result = await fetchTalks({ id, title, language, city, country, skip, limit, count_only }); 109 | 110 | if (!result.getTalks) { 111 | throw new Error('No results returned from API'); 112 | } 113 | 114 | if (count_only) { 115 | let response = `Total talks: ${result.getTalks.totalCount}`; 116 | 117 | if (group_by && result.getTalks.talks) { 118 | const counts = calculateGroupCounts(result.getTalks.talks, group_by); 119 | response += formatGroupCounts(counts, group_by); 120 | } 121 | 122 | const content: McpTextContent = { 123 | type: "text", 124 | text: response 125 | }; 126 | 127 | return { 128 | content: [content], 129 | }; 130 | } 131 | 132 | const content: McpTextContent = { 133 | type: "text", 134 | text: `Talks Results:\n\n${JSON.stringify(result.getTalks, null, 2)}` 135 | }; 136 | 137 | return { 138 | content: [content], 139 | }; 140 | } catch (error) { 141 | throw new Error(`Failed to fetch talks: ${error.message}`); 142 | } 143 | } 144 | }; -------------------------------------------------------------------------------- /src/services/api.ts: -------------------------------------------------------------------------------- 1 | import { client } from '../config/api.ts'; 2 | import type { 3 | TalksResponse, 4 | PostsResponse, 5 | VideosResponse, 6 | StatusResponse, 7 | Talk 8 | } from '../types/index.ts'; 9 | import { getLanguageCode } from '../utils/language.ts'; 10 | 11 | /** 12 | * Fetches talks with optional filtering and pagination 13 | */ 14 | export async function fetchTalks(params: { 15 | id?: string; 16 | title?: string; 17 | language?: string; 18 | city?: string; 19 | country?: string; 20 | skip?: number; 21 | limit?: number; 22 | count_only?: boolean; 23 | }): Promise { 24 | const { id, title, language, city, country, skip, limit, count_only } = params; 25 | const languageCode = getLanguageCode(language); 26 | 27 | return await client.query({ 28 | getTalks: { 29 | __args: { 30 | _id: id, 31 | title, 32 | language: languageCode, 33 | city, 34 | country, 35 | skip, 36 | limit: count_only ? 0 : limit, 37 | }, 38 | totalCount: true, 39 | retrieved: true, 40 | processedIn: true, 41 | talks: count_only ? { 42 | language: true, 43 | location: { 44 | country: true, 45 | city: true, 46 | } 47 | } : { 48 | _id: true, 49 | title: true, 50 | abstract: true, 51 | type: true, 52 | event: { 53 | link: true, 54 | name: true, 55 | }, 56 | slides: true, 57 | video: true, 58 | tags: true, 59 | location: { 60 | country: true, 61 | city: true, 62 | }, 63 | language: true, 64 | date: true, 65 | }, 66 | }, 67 | }) as TalksResponse; 68 | } 69 | 70 | /** 71 | * Fetches all talks for a specific year using pagination 72 | */ 73 | export async function fetchTalksByYear(params: { 74 | id?: string; 75 | title?: string; 76 | language?: string; 77 | city?: string; 78 | country?: string; 79 | year: number; 80 | }): Promise { 81 | const { id, title, language, city, country, year } = params; 82 | const languageCode = getLanguageCode(language); 83 | 84 | const allTalks: Talk[] = []; 85 | let currentSkip = 0; 86 | const BATCH_SIZE = 50; 87 | let shouldContinue = true; 88 | 89 | while (shouldContinue) { 90 | const result = await client.query({ 91 | getTalks: { 92 | __args: { 93 | _id: id, 94 | title, 95 | language: languageCode, 96 | city, 97 | country, 98 | skip: currentSkip, 99 | limit: BATCH_SIZE, 100 | }, 101 | totalCount: true, 102 | retrieved: true, 103 | processedIn: true, 104 | talks: { 105 | _id: true, 106 | title: true, 107 | abstract: true, 108 | type: true, 109 | event: { 110 | link: true, 111 | name: true, 112 | }, 113 | slides: true, 114 | video: true, 115 | tags: true, 116 | location: { 117 | country: true, 118 | city: true, 119 | }, 120 | language: true, 121 | date: true, 122 | }, 123 | }, 124 | }) as TalksResponse; 125 | 126 | if (!result.getTalks?.talks?.length) { 127 | shouldContinue = false; 128 | break; 129 | } 130 | 131 | const talks = result.getTalks.talks; 132 | const foundDifferentYear = talks?.some(talk => { 133 | if (!talk?.date) return false; 134 | const talkYear = new Date(talk.date).getFullYear(); 135 | return talkYear < year; 136 | }); 137 | 138 | // Filter talks for the specific year 139 | const yearFilteredTalks = talks?.filter(talk => { 140 | if (!talk?.date) return false; 141 | const talkYear = new Date(talk.date).getFullYear(); 142 | return talkYear === year; 143 | }) || []; 144 | 145 | allTalks.push(...yearFilteredTalks); 146 | 147 | if (foundDifferentYear) { 148 | shouldContinue = false; 149 | } else { 150 | currentSkip += BATCH_SIZE; 151 | } 152 | } 153 | 154 | return allTalks; 155 | } 156 | 157 | /** 158 | * Fetches posts with optional filtering and pagination 159 | */ 160 | export async function fetchPosts(params: { 161 | id?: string; 162 | title?: string; 163 | language?: string; 164 | portal?: string; 165 | skip?: number; 166 | limit?: number; 167 | }): Promise { 168 | const { id, title, language, portal, skip, limit } = params; 169 | const languageCode = getLanguageCode(language); 170 | 171 | return await client.query({ 172 | getPosts: { 173 | __args: { 174 | _id: id, 175 | title, 176 | language: languageCode, 177 | portal, 178 | skip, 179 | limit, 180 | }, 181 | totalCount: true, 182 | retrieved: true, 183 | processedIn: true, 184 | posts: { 185 | _id: true, 186 | title: true, 187 | abstract: true, 188 | type: true, 189 | link: true, 190 | additionalLinks: true, 191 | portal: { 192 | link: true, 193 | name: true, 194 | }, 195 | tags: true, 196 | language: true, 197 | date: true, 198 | }, 199 | }, 200 | }) as PostsResponse; 201 | } 202 | 203 | /** 204 | * Fetches videos with optional filtering and pagination 205 | */ 206 | export async function fetchVideos(params: { 207 | id?: string; 208 | title?: string; 209 | language?: string; 210 | skip?: number; 211 | limit?: number; 212 | }): Promise { 213 | const { id, title, language, skip, limit } = params; 214 | const languageCode = getLanguageCode(language); 215 | 216 | return await client.query({ 217 | getVideos: { 218 | __args: { 219 | _id: id, 220 | title, 221 | language: languageCode, 222 | skip, 223 | limit, 224 | }, 225 | totalCount: true, 226 | retrieved: true, 227 | processedIn: true, 228 | videos: { 229 | _id: true, 230 | title: true, 231 | abstract: true, 232 | type: true, 233 | link: true, 234 | additionalLinks: true, 235 | tags: true, 236 | language: true, 237 | date: true, 238 | }, 239 | }, 240 | }) as VideosResponse; 241 | } 242 | 243 | /** 244 | * Checks if the API is alive and responding 245 | */ 246 | export async function checkApiStatus(): Promise { 247 | return await client.query({ 248 | isAlive: true, 249 | }) as StatusResponse; 250 | } -------------------------------------------------------------------------------- /erickwendel-sdk/runtime/linkTypeMap.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import type { 3 | CompressedType, 4 | CompressedTypeMap, 5 | LinkedArgMap, 6 | LinkedField, 7 | LinkedType, 8 | LinkedTypeMap, 9 | } from './types' 10 | 11 | export interface PartialLinkedFieldMap { 12 | [field: string]: { 13 | type: string 14 | args?: LinkedArgMap 15 | } 16 | } 17 | 18 | export const linkTypeMap = ( 19 | typeMap: CompressedTypeMap, 20 | ): LinkedTypeMap => { 21 | const indexToName: Record = Object.assign( 22 | {}, 23 | ...Object.keys(typeMap.types).map((k, i) => ({ [i]: k })), 24 | ) 25 | 26 | let intermediaryTypeMap = Object.assign( 27 | {}, 28 | ...Object.keys(typeMap.types || {}).map( 29 | (k): Record => { 30 | const type: CompressedType = typeMap.types[k]! 31 | const fields = type || {} 32 | return { 33 | [k]: { 34 | name: k, 35 | // type scalar properties 36 | scalar: Object.keys(fields).filter((f) => { 37 | const [type] = fields[f] || [] 38 | 39 | const isScalar = 40 | type && typeMap.scalars.includes(type) 41 | if (!isScalar) { 42 | return false 43 | } 44 | const args = fields[f]?.[1] 45 | const argTypes = Object.values(args || {}) 46 | .map((x) => x?.[1]) 47 | .filter(Boolean) 48 | 49 | const hasRequiredArgs = argTypes.some( 50 | (str) => str && str.endsWith('!'), 51 | ) 52 | if (hasRequiredArgs) { 53 | return false 54 | } 55 | return true 56 | }), 57 | // fields with corresponding `type` and `args` 58 | fields: Object.assign( 59 | {}, 60 | ...Object.keys(fields).map( 61 | (f): PartialLinkedFieldMap => { 62 | const [typeIndex, args] = fields[f] || [] 63 | if (typeIndex == null) { 64 | return {} 65 | } 66 | return { 67 | [f]: { 68 | // replace index with type name 69 | type: indexToName[typeIndex], 70 | args: Object.assign( 71 | {}, 72 | ...Object.keys(args || {}).map( 73 | (k) => { 74 | // if argTypeString == argTypeName, argTypeString is missing, need to readd it 75 | if (!args || !args[k]) { 76 | return 77 | } 78 | const [ 79 | argTypeName, 80 | argTypeString, 81 | ] = args[k] as any 82 | return { 83 | [k]: [ 84 | indexToName[ 85 | argTypeName 86 | ], 87 | argTypeString || 88 | indexToName[ 89 | argTypeName 90 | ], 91 | ], 92 | } 93 | }, 94 | ), 95 | ), 96 | }, 97 | } 98 | }, 99 | ), 100 | ), 101 | }, 102 | } 103 | }, 104 | ), 105 | ) 106 | const res = resolveConcreteTypes(intermediaryTypeMap) 107 | return res 108 | } 109 | 110 | // replace typename with concrete type 111 | export const resolveConcreteTypes = (linkedTypeMap: LinkedTypeMap) => { 112 | Object.keys(linkedTypeMap).forEach((typeNameFromKey) => { 113 | const type: LinkedType = linkedTypeMap[typeNameFromKey]! 114 | // type.name = typeNameFromKey 115 | if (!type.fields) { 116 | return 117 | } 118 | 119 | const fields = type.fields 120 | 121 | Object.keys(fields).forEach((f) => { 122 | const field: LinkedField = fields[f]! 123 | 124 | if (field.args) { 125 | const args = field.args 126 | Object.keys(args).forEach((key) => { 127 | const arg = args[key] 128 | 129 | if (arg) { 130 | const [typeName] = arg 131 | 132 | if (typeof typeName === 'string') { 133 | if (!linkedTypeMap[typeName]) { 134 | linkedTypeMap[typeName] = { name: typeName } 135 | } 136 | 137 | arg[0] = linkedTypeMap[typeName]! 138 | } 139 | } 140 | }) 141 | } 142 | 143 | const typeName = field.type as LinkedType | string 144 | 145 | if (typeof typeName === 'string') { 146 | if (!linkedTypeMap[typeName]) { 147 | linkedTypeMap[typeName] = { name: typeName } 148 | } 149 | 150 | field.type = linkedTypeMap[typeName]! 151 | } 152 | }) 153 | }) 154 | 155 | return linkedTypeMap 156 | } 157 | -------------------------------------------------------------------------------- /erickwendel-sdk/runtime/generateGraphqlOperation.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import type { LinkedField, LinkedType } from './types' 3 | 4 | export interface Args { 5 | [arg: string]: any | undefined 6 | } 7 | 8 | export interface Fields { 9 | [field: string]: Request 10 | } 11 | 12 | export type Request = boolean | number | Fields 13 | 14 | export interface Variables { 15 | [name: string]: { 16 | value: any 17 | typing: [LinkedType, string] 18 | } 19 | } 20 | 21 | export interface Context { 22 | root: LinkedType 23 | varCounter: number 24 | variables: Variables 25 | fragmentCounter: number 26 | fragments: string[] 27 | } 28 | 29 | export interface GraphqlOperation { 30 | query: string 31 | variables?: { [name: string]: any } 32 | operationName?: string 33 | } 34 | 35 | const parseRequest = ( 36 | request: Request | undefined, 37 | ctx: Context, 38 | path: string[], 39 | ): string => { 40 | if (typeof request === 'object' && '__args' in request) { 41 | const args: any = request.__args 42 | let fields: Request | undefined = { ...request } 43 | delete fields.__args 44 | const argNames = Object.keys(args) 45 | 46 | if (argNames.length === 0) { 47 | return parseRequest(fields, ctx, path) 48 | } 49 | 50 | const field = getFieldFromPath(ctx.root, path) 51 | 52 | const argStrings = argNames.map((argName) => { 53 | ctx.varCounter++ 54 | const varName = `v${ctx.varCounter}` 55 | 56 | const typing = field.args && field.args[argName] // typeMap used here, .args 57 | 58 | if (!typing) { 59 | throw new Error( 60 | `no typing defined for argument \`${argName}\` in path \`${path.join( 61 | '.', 62 | )}\``, 63 | ) 64 | } 65 | 66 | ctx.variables[varName] = { 67 | value: args[argName], 68 | typing, 69 | } 70 | 71 | return `${argName}:$${varName}` 72 | }) 73 | return `(${argStrings})${parseRequest(fields, ctx, path)}` 74 | } else if (typeof request === 'object' && Object.keys(request).length > 0) { 75 | const fields = request 76 | const fieldNames = Object.keys(fields).filter((k) => Boolean(fields[k])) 77 | 78 | if (fieldNames.length === 0) { 79 | throw new Error( 80 | `field selection should not be empty: ${path.join('.')}`, 81 | ) 82 | } 83 | 84 | const type = 85 | path.length > 0 ? getFieldFromPath(ctx.root, path).type : ctx.root 86 | const scalarFields = type.scalar 87 | 88 | let scalarFieldsFragment: string | undefined 89 | 90 | if (fieldNames.includes('__scalar')) { 91 | const falsyFieldNames = new Set( 92 | Object.keys(fields).filter((k) => !Boolean(fields[k])), 93 | ) 94 | if (scalarFields?.length) { 95 | ctx.fragmentCounter++ 96 | scalarFieldsFragment = `f${ctx.fragmentCounter}` 97 | 98 | ctx.fragments.push( 99 | `fragment ${scalarFieldsFragment} on ${ 100 | type.name 101 | }{${scalarFields 102 | .filter((f) => !falsyFieldNames.has(f)) 103 | .join(',')}}`, 104 | ) 105 | } 106 | } 107 | 108 | const fieldsSelection = fieldNames 109 | .filter((f) => !['__scalar', '__name'].includes(f)) 110 | .map((f) => { 111 | const parsed = parseRequest(fields[f], ctx, [...path, f]) 112 | 113 | if (f.startsWith('on_')) { 114 | ctx.fragmentCounter++ 115 | const implementationFragment = `f${ctx.fragmentCounter}` 116 | 117 | const typeMatch = f.match(/^on_(.+)/) 118 | 119 | if (!typeMatch || !typeMatch[1]) 120 | throw new Error('match failed') 121 | 122 | ctx.fragments.push( 123 | `fragment ${implementationFragment} on ${typeMatch[1]}${parsed}`, 124 | ) 125 | 126 | return `...${implementationFragment}` 127 | } else { 128 | return `${f}${parsed}` 129 | } 130 | }) 131 | .concat(scalarFieldsFragment ? [`...${scalarFieldsFragment}`] : []) 132 | .join(',') 133 | 134 | return `{${fieldsSelection}}` 135 | } else { 136 | return '' 137 | } 138 | } 139 | 140 | export const generateGraphqlOperation = ( 141 | operation: 'query' | 'mutation' | 'subscription', 142 | root: LinkedType, 143 | fields?: Fields, 144 | ): GraphqlOperation => { 145 | const ctx: Context = { 146 | root: root, 147 | varCounter: 0, 148 | variables: {}, 149 | fragmentCounter: 0, 150 | fragments: [], 151 | } 152 | const result = parseRequest(fields, ctx, []) 153 | 154 | const varNames = Object.keys(ctx.variables) 155 | 156 | const varsString = 157 | varNames.length > 0 158 | ? `(${varNames.map((v) => { 159 | const variableType = ctx.variables[v].typing[1] 160 | return `$${v}:${variableType}` 161 | })})` 162 | : '' 163 | 164 | const operationName = fields?.__name || '' 165 | 166 | return { 167 | query: [ 168 | `${operation} ${operationName}${varsString}${result}`, 169 | ...ctx.fragments, 170 | ].join(','), 171 | variables: Object.keys(ctx.variables).reduce<{ [name: string]: any }>( 172 | (r, v) => { 173 | r[v] = ctx.variables[v].value 174 | return r 175 | }, 176 | {}, 177 | ), 178 | ...(operationName ? { operationName: operationName.toString() } : {}), 179 | } 180 | } 181 | 182 | export const getFieldFromPath = ( 183 | root: LinkedType | undefined, 184 | path: string[], 185 | ) => { 186 | let current: LinkedField | undefined 187 | 188 | if (!root) throw new Error('root type is not provided') 189 | 190 | if (path.length === 0) throw new Error(`path is empty`) 191 | 192 | path.forEach((f) => { 193 | const type = current ? current.type : root 194 | 195 | if (!type.fields) 196 | throw new Error(`type \`${type.name}\` does not have fields`) 197 | 198 | const possibleTypes = Object.keys(type.fields) 199 | .filter((i) => i.startsWith('on_')) 200 | .reduce( 201 | (types, fieldName) => { 202 | const field = type.fields && type.fields[fieldName] 203 | if (field) types.push(field.type) 204 | return types 205 | }, 206 | [type], 207 | ) 208 | 209 | let field: LinkedField | null = null 210 | 211 | possibleTypes.forEach((type) => { 212 | const found = type.fields && type.fields[f] 213 | if (found) field = found 214 | }) 215 | 216 | if (!field) 217 | throw new Error( 218 | `type \`${type.name}\` does not have a field \`${f}\``, 219 | ) 220 | 221 | current = field 222 | }) 223 | 224 | return current as LinkedField 225 | } 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # erickwendel-contributions-mcp 2 | 3 | ![CI Status](https://github.com/ErickWendel/erickwendel-contributions-mcp/workflows/Test%20MCP%20Server/badge.svg) 4 | [![smithery badge](https://smithery.ai/badge/@ErickWendel/erickwendel-contributions-mcp)](https://smithery.ai/server/@ErickWendel/erickwendel-contributions-mcp) 5 | 6 | A Model Context Protocol (MCP) server that provides tools to query [Erick Wendel's contributions](https://erickwendel.com.br/) across different platforms. Query talks, blog posts, and videos using natural language through Claude, Cursor or similars. This project was built using [Cursor](https://cursor.sh) IDE with the default agent (trial version). 7 | 8 | This MCP server is also available on [Smithery](https://smithery.ai/server/@ErickWendel/erickwendel-contributions-mcp) for direct integration. 9 | 10 | ## Available Tools 11 | 12 | This MCP server provides the following tools to interact with the API: 13 | 14 | - `get-talks`: Retrieves a paginated list of talks with optional filtering 15 | - Supports filtering by ID, title, language, city, country, and year 16 | - Can return counts grouped by language, country, or city 17 | 18 | - `get-posts`: Fetches posts with optional filtering and pagination 19 | - Supports filtering by ID, title, language, and portal 20 | 21 | - `get-videos`: Retrieves videos with optional filtering and pagination 22 | - Supports filtering by ID, title, and language 23 | 24 | - `check-status`: Verifies if the API is alive and responding 25 | 26 | # Integration with AI Tools 27 | 28 | ## Inspect MCP Server Capabilities 29 | 30 | You can inspect this MCP server's capabilities using Smithery: 31 | 32 | ```bash 33 | npx -y @smithery/cli@latest inspect @ErickWendel/erickwendel-contributions-mcp 34 | ``` 35 | 36 | This will show you all available tools, their parameters, and how to use them. 37 | 38 | ## Setup 39 | 40 | 1. Make sure you're using Node.js v23+ 41 | ```bash 42 | node -v 43 | #v23.9.0 44 | ``` 45 | 46 | 2. Clone this repository: 47 | ```bash 48 | git clone https://github.com/erickwendel/erickwendel-contributions-mcp.git 49 | cd erickwendel-contributions-mcp 50 | ``` 51 | 52 | 3. Restore dependencies: 53 | ```bash 54 | npm ci 55 | ``` 56 | 57 | ## Integration with AI Tools 58 | 59 | ### Cursor Setup 60 | 61 | 1. Open Cursor Settings 62 | 2. Navigate to MCP section 63 | 3. Click "Add new MCP server" 64 | 4. Configure the server: 65 | ``` 66 | Name = erickwendel-contributions 67 | Type = command 68 | Command = node ABSOLUTE_PATH_TO_PROJECT/src/index.ts 69 | ``` 70 | 71 | or if you prefer executing it from Smithery 72 | ``` 73 | Name = erickwendel-contributions 74 | Type = command 75 | Command = npm exec -- @smithery/cli@latest run @ErickWendel/erickwendel-contributions-mcp 76 | ``` 77 | ![](./demos/cursor-mcp.png) 78 | 79 | or configure directly from the Cursor's global MCP file located in `~/.cursor/mcp.json` and add the following: 80 | 81 | ```json 82 | { 83 | "mcpServers": { 84 | "erickwendel-contributions": { 85 | "command": "node", 86 | "args": ["ABSOLUTE_PATH_TO_PROJECT/src/index.ts"] 87 | } 88 | } 89 | } 90 | ``` 91 | or if you prefer executing it from Smithery 92 | ```json 93 | { 94 | "mcpServers": { 95 | "erickwendel-contributions": { 96 | "command": "npm", 97 | "args": [ 98 | "exec", 99 | "--", 100 | "@smithery/cli@latest", 101 | "run", 102 | "@ErickWendel/erickwendel-contributions-mcp" 103 | ] 104 | } 105 | } 106 | } 107 | ``` 108 | 109 | 5. Make sure Cursor chat is in Agent mode by selecting "Agent" in the lower left side dropdown 110 | 111 | 6. Go to the chat an ask "how many videos were published about JavaScript in 2024" 112 | 113 | ![](./demos/cursor-videos-in-2024.png) 114 | 115 | ### Claude Desktop Setup 116 | 117 | #### Installing via Smithery 118 | 119 | To install Erick Wendel Contributions for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@ErickWendel/erickwendel-contributions-mcp): 120 | 121 | ```bash 122 | npx -y @smithery/cli install @ErickWendel/erickwendel-contributions-mcp --client claude 123 | ``` 124 | 125 | > **Note**: The Smithery CLI installation for Claude is currently experiencing issues. Please use the manual installation method below until this is resolved. 126 | 127 | #### Manual Setup 128 | 129 | 1. Go to Claude settings 130 | 2. Click in the Developer tab 131 | 3. Click in edit config 132 | 4. Open the config in a code editor 133 | 5. Add the following configuration to your Claude Desktop config: 134 | 135 | ```json 136 | { 137 | "mcpServers": { 138 | "erickwendel-contributions": { 139 | "command": "node", 140 | "args": ["ABSOLUTE_PATH_TO_PROJECT/src/index.ts"] 141 | } 142 | } 143 | } 144 | ``` 145 | or if you prefer executing it from Smithery 146 | ```json 147 | { 148 | "mcpServers": { 149 | "erickwendel-contributions": { 150 | "command": "npm", 151 | "args": [ 152 | "exec", 153 | "--", 154 | "@smithery/cli@latest", 155 | "run", 156 | "@ErickWendel/erickwendel-contributions-mcp" 157 | ] 158 | } 159 | } 160 | } 161 | ``` 162 | 163 | 6. Save file and Restart Claude Desktop 164 | 7. Open the Developer tab again and check if it's in the "running" state as follows: 165 | 166 | ![](./demos/claude-conf.png) 167 | 168 | 8. Go to the chat and ask "Are there videos about RAG?" 169 | 170 | ![](./demos/videos-about-rag.jpeg) 171 | 172 | ### Free Alternative Using MCPHost 173 | 174 | If you don't have access to Claude Desktop nor Cursor, you can use [MCPHost](https://github.com/mark3labs/mcphost) with Ollama as a free alternative. MCPHost is a CLI tool that enables Large Language Models to interact with MCP servers. 175 | 176 | 1. Install MCPHost: 177 | ```bash 178 | go install github.com/mark3labs/mcphost@latest 179 | ``` 180 | 181 | 2. Create a config file (e.g. [./mcp.jsonc](./mcp.jsonc)): 182 | ```json 183 | { 184 | "mcpServers": { 185 | "erickwendel-contributions": { 186 | "command": "node", 187 | "args": ["ABSOLUTE_PATH_TO_PROJECT/src/index.ts"] 188 | } 189 | } 190 | } 191 | ``` 192 | or if you prefer executing it from Smithery 193 | ```json 194 | { 195 | "mcpServers": { 196 | "erickwendel-contributions": { 197 | "command": "npm", 198 | "args": [ 199 | "exec", 200 | "--", 201 | "@smithery/cli@latest", 202 | "run", 203 | "@ErickWendel/erickwendel-contributions-mcp" 204 | ] 205 | } 206 | } 207 | } 208 | ``` 209 | 3. Run MCPHost with your preferred Ollama model: 210 | ```bash 211 | ollama pull MODEL_NAME 212 | mcphost --config ./mcp.jsonc -m ollama:MODEL_NAME 213 | ``` 214 | 215 | ## Example Queries 216 | 217 | Here are some examples of queries you can ask Claude, Cursor or any MCP Client: 218 | 219 | 1. "How many talks were given in 2023?" 220 | 221 | ![](./demos/talks-in-2023.jpeg) 222 | 223 | 2. "Show me talks in Spanish" 224 | 225 | ![](./demos/talks-in-spanish.jpeg) 226 | 227 | 3. "Find posts about WebXR" 228 | 229 | ![](./demos/posts-webxr.jpeg) 230 | 231 | 232 | # Development 233 | ## Features 234 | 235 | - Built with Model Context Protocol (MCP) 236 | - Type-safe with TypeScript and Zod schema validation 237 | - Native TypeScript support in Node.js without transpilation 238 | - Generated SDK using [GenQL](https://genql.dev) 239 | - Modular architecture with separation of concerns 240 | - Standard I/O transport for easy integration 241 | - Structured error handling 242 | - Compatible with Claude Desktop, Cursor, and [MCPHost](https://github.com/mark3labs/mcphost) (free alternative) 243 | 244 | > Note: This project requires Node.js v23+ as it uses the native TypeScript support added in the last year. 245 | 246 | ## Architecture 247 | 248 | The codebase follows a modular structure: 249 | 250 | ``` 251 | src/ 252 | ├── config/ # Configuration settings 253 | ├── types/ # TypeScript interfaces and types 254 | ├── tools/ # MCP tool implementations 255 | ├── utils/ # Utility functions 256 | ├── services/ # API service layer 257 | └── index.ts # Main entry point 258 | ``` 259 | 260 | ## Testing 261 | 262 | To run the test suite: 263 | 264 | ```bash 265 | npm test 266 | ``` 267 | 268 | For development mode with watch: 269 | 270 | ```bash 271 | npm run test:dev 272 | ``` 273 | 274 | ## Contributing 275 | 276 | Contributions are welcome! Please feel free to submit a Pull Request. 277 | 278 | ## Author 279 | 280 | [Erick Wendel](https://linktr.ee/erickwendel) 281 | 282 | ## License 283 | 284 | This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details. -------------------------------------------------------------------------------- /erickwendel-sdk/runtime/batcher.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import type { GraphqlOperation } from './generateGraphqlOperation.ts' 3 | import { GenqlError } from './error.ts' 4 | 5 | type Variables = Record 6 | 7 | type QueryError = Error & { 8 | message: string 9 | 10 | locations?: Array<{ 11 | line: number 12 | column: number 13 | }> 14 | path?: any 15 | rid: string 16 | details?: Record 17 | } 18 | type Result = { 19 | data: Record 20 | errors: Array 21 | } 22 | type Fetcher = ( 23 | batchedQuery: GraphqlOperation | Array, 24 | ) => Promise> 25 | type Options = { 26 | batchInterval?: number 27 | shouldBatch?: boolean 28 | maxBatchSize?: number 29 | } 30 | type Queue = Array<{ 31 | request: GraphqlOperation 32 | resolve: (...args: Array) => any 33 | reject: (...args: Array) => any 34 | }> 35 | 36 | /** 37 | * takes a list of requests (queue) and batches them into a single server request. 38 | * It will then resolve each individual requests promise with the appropriate data. 39 | * @private 40 | * @param {QueryBatcher} client - the client to use 41 | * @param {Queue} queue - the list of requests to batch 42 | */ 43 | function dispatchQueueBatch(client: QueryBatcher, queue: Queue): void { 44 | let batchedQuery: any = queue.map((item) => item.request) 45 | 46 | if (batchedQuery.length === 1) { 47 | batchedQuery = batchedQuery[0] 48 | } 49 | (() => { 50 | try { 51 | return client.fetcher(batchedQuery); 52 | } catch(e) { 53 | return Promise.reject(e); 54 | } 55 | })().then((responses: any) => { 56 | if (queue.length === 1 && !Array.isArray(responses)) { 57 | if (responses.errors && responses.errors.length) { 58 | queue[0].reject( 59 | new GenqlError(responses.errors, responses.data), 60 | ) 61 | return 62 | } 63 | 64 | queue[0].resolve(responses) 65 | return 66 | } else if (responses.length !== queue.length) { 67 | throw new Error('response length did not match query length') 68 | } 69 | 70 | for (let i = 0; i < queue.length; i++) { 71 | if (responses[i].errors && responses[i].errors.length) { 72 | queue[i].reject( 73 | new GenqlError(responses[i].errors, responses[i].data), 74 | ) 75 | } else { 76 | queue[i].resolve(responses[i]) 77 | } 78 | } 79 | }) 80 | .catch((e) => { 81 | for (let i = 0; i < queue.length; i++) { 82 | queue[i].reject(e) 83 | } 84 | }); 85 | } 86 | 87 | /** 88 | * creates a list of requests to batch according to max batch size. 89 | * @private 90 | * @param {QueryBatcher} client - the client to create list of requests from from 91 | * @param {Options} options - the options for the batch 92 | */ 93 | function dispatchQueue(client: QueryBatcher, options: Options): void { 94 | const queue = client._queue 95 | const maxBatchSize = options.maxBatchSize || 0 96 | client._queue = [] 97 | 98 | if (maxBatchSize > 0 && maxBatchSize < queue.length) { 99 | for (let i = 0; i < queue.length / maxBatchSize; i++) { 100 | dispatchQueueBatch( 101 | client, 102 | queue.slice(i * maxBatchSize, (i + 1) * maxBatchSize), 103 | ) 104 | } 105 | } else { 106 | dispatchQueueBatch(client, queue) 107 | } 108 | } 109 | /** 110 | * Create a batcher client. 111 | * @param {Fetcher} fetcher - A function that can handle the network requests to graphql endpoint 112 | * @param {Options} options - the options to be used by client 113 | * @param {boolean} options.shouldBatch - should the client batch requests. (default true) 114 | * @param {integer} options.batchInterval - duration (in MS) of each batch window. (default 6) 115 | * @param {integer} options.maxBatchSize - max number of requests in a batch. (default 0) 116 | * @param {boolean} options.defaultHeaders - default headers to include with every request 117 | * 118 | * @example 119 | * const fetcher = batchedQuery => fetch('path/to/graphql', { 120 | * method: 'post', 121 | * headers: { 122 | * Accept: 'application/json', 123 | * 'Content-Type': 'application/json', 124 | * }, 125 | * body: JSON.stringify(batchedQuery), 126 | * credentials: 'include', 127 | * }) 128 | * .then(response => response.json()) 129 | * 130 | * const client = new QueryBatcher(fetcher, { maxBatchSize: 10 }) 131 | */ 132 | 133 | export class QueryBatcher { 134 | fetcher: Fetcher 135 | _options: Options 136 | _queue: Queue 137 | 138 | constructor( 139 | fetcher: Fetcher, 140 | { 141 | batchInterval = 6, 142 | shouldBatch = true, 143 | maxBatchSize = 0, 144 | }: Options = {}, 145 | ) { 146 | this.fetcher = fetcher 147 | this._options = { 148 | batchInterval, 149 | shouldBatch, 150 | maxBatchSize, 151 | } 152 | this._queue = [] 153 | } 154 | 155 | /** 156 | * Fetch will send a graphql request and return the parsed json. 157 | * @param {string} query - the graphql query. 158 | * @param {Variables} variables - any variables you wish to inject as key/value pairs. 159 | * @param {[string]} operationName - the graphql operationName. 160 | * @param {Options} overrides - the client options overrides. 161 | * 162 | * @return {promise} resolves to parsed json of server response 163 | * 164 | * @example 165 | * client.fetch(` 166 | * query getHuman($id: ID!) { 167 | * human(id: $id) { 168 | * name 169 | * height 170 | * } 171 | * } 172 | * `, { id: "1001" }, 'getHuman') 173 | * .then(human => { 174 | * // do something with human 175 | * console.log(human); 176 | * }); 177 | */ 178 | fetch( 179 | query: string, 180 | variables?: Variables, 181 | operationName?: string, 182 | overrides: Options = {}, 183 | ): Promise { 184 | const request: GraphqlOperation = { 185 | query, 186 | } 187 | const options = Object.assign({}, this._options, overrides) 188 | 189 | if (variables) { 190 | request.variables = variables 191 | } 192 | 193 | if (operationName) { 194 | request.operationName = operationName 195 | } 196 | 197 | const promise = new Promise((resolve, reject) => { 198 | this._queue.push({ 199 | request, 200 | resolve, 201 | reject, 202 | }) 203 | 204 | if (this._queue.length === 1) { 205 | if (options.shouldBatch) { 206 | setTimeout( 207 | () => dispatchQueue(this, options), 208 | options.batchInterval, 209 | ) 210 | } else { 211 | dispatchQueue(this, options) 212 | } 213 | } 214 | }) 215 | return promise 216 | } 217 | 218 | /** 219 | * Fetch will send a graphql request and return the parsed json. 220 | * @param {string} query - the graphql query. 221 | * @param {Variables} variables - any variables you wish to inject as key/value pairs. 222 | * @param {[string]} operationName - the graphql operationName. 223 | * @param {Options} overrides - the client options overrides. 224 | * 225 | * @return {Promise>} resolves to parsed json of server response 226 | * 227 | * @example 228 | * client.forceFetch(` 229 | * query getHuman($id: ID!) { 230 | * human(id: $id) { 231 | * name 232 | * height 233 | * } 234 | * } 235 | * `, { id: "1001" }, 'getHuman') 236 | * .then(human => { 237 | * // do something with human 238 | * console.log(human); 239 | * }); 240 | */ 241 | forceFetch( 242 | query: string, 243 | variables?: Variables, 244 | operationName?: string, 245 | overrides: Options = {}, 246 | ): Promise { 247 | const request: GraphqlOperation = { 248 | query, 249 | } 250 | const options = Object.assign({}, this._options, overrides, { 251 | shouldBatch: false, 252 | }) 253 | 254 | if (variables) { 255 | request.variables = variables 256 | } 257 | 258 | if (operationName) { 259 | request.operationName = operationName 260 | } 261 | 262 | const promise = new Promise((resolve, reject) => { 263 | const client = new QueryBatcher(this.fetcher, this._options) 264 | client._queue = [ 265 | { 266 | request, 267 | resolve, 268 | reject, 269 | }, 270 | ] 271 | dispatchQueue(client, options) 272 | }) 273 | return promise 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /erickwendel-sdk/types.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "scalars": [ 3 | 1, 4 | 2, 5 | 3, 6 | 9, 7 | 10 8 | ], 9 | "types": { 10 | "Query": { 11 | "getTalks": [ 12 | 4, 13 | { 14 | "_id": [ 15 | 1 16 | ], 17 | "title": [ 18 | 2 19 | ], 20 | "language": [ 21 | 2 22 | ], 23 | "city": [ 24 | 2 25 | ], 26 | "country": [ 27 | 2 28 | ], 29 | "skip": [ 30 | 3 31 | ], 32 | "limit": [ 33 | 3 34 | ] 35 | } 36 | ], 37 | "isAlive": [ 38 | 10 39 | ], 40 | "getPosts": [ 41 | 11, 42 | { 43 | "_id": [ 44 | 1 45 | ], 46 | "title": [ 47 | 2 48 | ], 49 | "language": [ 50 | 2 51 | ], 52 | "portal": [ 53 | 2 54 | ], 55 | "skip": [ 56 | 3 57 | ], 58 | "limit": [ 59 | 3 60 | ] 61 | } 62 | ], 63 | "getVideos": [ 64 | 14, 65 | { 66 | "_id": [ 67 | 1 68 | ], 69 | "title": [ 70 | 2 71 | ], 72 | "language": [ 73 | 2 74 | ], 75 | "skip": [ 76 | 3 77 | ], 78 | "limit": [ 79 | 3 80 | ] 81 | } 82 | ], 83 | "getProjects": [ 84 | 16, 85 | { 86 | "_id": [ 87 | 1 88 | ], 89 | "title": [ 90 | 2 91 | ], 92 | "language": [ 93 | 2 94 | ], 95 | "skip": [ 96 | 3 97 | ], 98 | "limit": [ 99 | 3 100 | ] 101 | } 102 | ], 103 | "__typename": [ 104 | 2 105 | ] 106 | }, 107 | "ID": {}, 108 | "String": {}, 109 | "Int": {}, 110 | "TalkQuery": { 111 | "totalCount": [ 112 | 3 113 | ], 114 | "retrieved": [ 115 | 3 116 | ], 117 | "processedIn": [ 118 | 3 119 | ], 120 | "talks": [ 121 | 5 122 | ], 123 | "__typename": [ 124 | 2 125 | ] 126 | }, 127 | "Talk": { 128 | "_id": [ 129 | 1 130 | ], 131 | "title": [ 132 | 2 133 | ], 134 | "abstract": [ 135 | 2 136 | ], 137 | "type": [ 138 | 2 139 | ], 140 | "event": [ 141 | 6 142 | ], 143 | "slides": [ 144 | 2 145 | ], 146 | "images": [ 147 | 7 148 | ], 149 | "video": [ 150 | 2 151 | ], 152 | "tags": [ 153 | 2 154 | ], 155 | "location": [ 156 | 8 157 | ], 158 | "additionalLinks": [ 159 | 2 160 | ], 161 | "language": [ 162 | 2 163 | ], 164 | "date": [ 165 | 2 166 | ], 167 | "__typename": [ 168 | 2 169 | ] 170 | }, 171 | "Event": { 172 | "link": [ 173 | 2 174 | ], 175 | "name": [ 176 | 2 177 | ], 178 | "__typename": [ 179 | 2 180 | ] 181 | }, 182 | "Image": { 183 | "url": [ 184 | 2 185 | ], 186 | "filename": [ 187 | 2 188 | ], 189 | "size": [ 190 | 2 191 | ], 192 | "pathId": [ 193 | 2 194 | ], 195 | "__typename": [ 196 | 2 197 | ] 198 | }, 199 | "Location": { 200 | "latitude": [ 201 | 9 202 | ], 203 | "longitude": [ 204 | 9 205 | ], 206 | "country": [ 207 | 2 208 | ], 209 | "uf": [ 210 | 2 211 | ], 212 | "city": [ 213 | 2 214 | ], 215 | "__typename": [ 216 | 2 217 | ] 218 | }, 219 | "Float": {}, 220 | "Boolean": {}, 221 | "PostQuery": { 222 | "totalCount": [ 223 | 3 224 | ], 225 | "retrieved": [ 226 | 3 227 | ], 228 | "processedIn": [ 229 | 3 230 | ], 231 | "posts": [ 232 | 12 233 | ], 234 | "__typename": [ 235 | 2 236 | ] 237 | }, 238 | "Post": { 239 | "_id": [ 240 | 1 241 | ], 242 | "title": [ 243 | 2 244 | ], 245 | "abstract": [ 246 | 2 247 | ], 248 | "type": [ 249 | 2 250 | ], 251 | "link": [ 252 | 2 253 | ], 254 | "additionalLinks": [ 255 | 2 256 | ], 257 | "portal": [ 258 | 13 259 | ], 260 | "tags": [ 261 | 2 262 | ], 263 | "language": [ 264 | 2 265 | ], 266 | "date": [ 267 | 2 268 | ], 269 | "__typename": [ 270 | 2 271 | ] 272 | }, 273 | "Portal": { 274 | "link": [ 275 | 2 276 | ], 277 | "name": [ 278 | 2 279 | ], 280 | "__typename": [ 281 | 2 282 | ] 283 | }, 284 | "VideoQuery": { 285 | "totalCount": [ 286 | 3 287 | ], 288 | "retrieved": [ 289 | 3 290 | ], 291 | "processedIn": [ 292 | 3 293 | ], 294 | "videos": [ 295 | 15 296 | ], 297 | "__typename": [ 298 | 2 299 | ] 300 | }, 301 | "Video": { 302 | "_id": [ 303 | 1 304 | ], 305 | "title": [ 306 | 2 307 | ], 308 | "abstract": [ 309 | 2 310 | ], 311 | "type": [ 312 | 2 313 | ], 314 | "link": [ 315 | 2 316 | ], 317 | "additionalLinks": [ 318 | 2 319 | ], 320 | "tags": [ 321 | 2 322 | ], 323 | "language": [ 324 | 2 325 | ], 326 | "date": [ 327 | 2 328 | ], 329 | "__typename": [ 330 | 2 331 | ] 332 | }, 333 | "ProjectQuery": { 334 | "totalCount": [ 335 | 3 336 | ], 337 | "retrieved": [ 338 | 3 339 | ], 340 | "processedIn": [ 341 | 3 342 | ], 343 | "projects": [ 344 | 17 345 | ], 346 | "__typename": [ 347 | 2 348 | ] 349 | }, 350 | "Project": { 351 | "_id": [ 352 | 1 353 | ], 354 | "title": [ 355 | 2 356 | ], 357 | "abstract": [ 358 | 2 359 | ], 360 | "type": [ 361 | 2 362 | ], 363 | "link": [ 364 | 2 365 | ], 366 | "additionalLinks": [ 367 | 2 368 | ], 369 | "tags": [ 370 | 2 371 | ], 372 | "language": [ 373 | 2 374 | ], 375 | "date": [ 376 | 2 377 | ], 378 | "__typename": [ 379 | 2 380 | ] 381 | } 382 | } 383 | } -------------------------------------------------------------------------------- /erickwendel-sdk/schema.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | /* istanbul ignore file */ 3 | /* tslint:disable */ 4 | /* eslint-disable */ 5 | 6 | export type Scalars = { 7 | ID: string, 8 | String: string, 9 | Int: number, 10 | Float: number, 11 | Boolean: boolean, 12 | } 13 | 14 | export interface Query { 15 | getTalks: (TalkQuery | null) 16 | isAlive: (Scalars['Boolean'] | null) 17 | getPosts: (PostQuery | null) 18 | getVideos: (VideoQuery | null) 19 | getProjects: (ProjectQuery | null) 20 | __typename: 'Query' 21 | } 22 | 23 | export interface TalkQuery { 24 | totalCount: Scalars['Int'] 25 | retrieved: Scalars['Int'] 26 | processedIn: Scalars['Int'] 27 | talks: ((Talk | null)[] | null) 28 | __typename: 'TalkQuery' 29 | } 30 | 31 | export interface Talk { 32 | _id: (Scalars['ID'] | null) 33 | title: (Scalars['String'] | null) 34 | abstract: (Scalars['String'] | null) 35 | type: (Scalars['String'] | null) 36 | event: (Event | null) 37 | slides: (Scalars['String'] | null) 38 | images: ((Image | null)[] | null) 39 | video: (Scalars['String'] | null) 40 | tags: ((Scalars['String'] | null)[] | null) 41 | location: (Location | null) 42 | additionalLinks: ((Scalars['String'] | null)[] | null) 43 | language: (Scalars['String'] | null) 44 | date: (Scalars['String'] | null) 45 | __typename: 'Talk' 46 | } 47 | 48 | export interface Event { 49 | link: (Scalars['String'] | null) 50 | name: (Scalars['String'] | null) 51 | __typename: 'Event' 52 | } 53 | 54 | export interface Image { 55 | url: (Scalars['String'] | null) 56 | filename: (Scalars['String'] | null) 57 | size: (Scalars['String'] | null) 58 | pathId: (Scalars['String'] | null) 59 | __typename: 'Image' 60 | } 61 | 62 | export interface Location { 63 | latitude: (Scalars['Float'] | null) 64 | longitude: (Scalars['Float'] | null) 65 | country: (Scalars['String'] | null) 66 | uf: (Scalars['String'] | null) 67 | city: (Scalars['String'] | null) 68 | __typename: 'Location' 69 | } 70 | 71 | export interface PostQuery { 72 | totalCount: Scalars['Int'] 73 | retrieved: Scalars['Int'] 74 | processedIn: Scalars['Int'] 75 | posts: ((Post | null)[] | null) 76 | __typename: 'PostQuery' 77 | } 78 | 79 | export interface Post { 80 | _id: (Scalars['ID'] | null) 81 | title: (Scalars['String'] | null) 82 | abstract: (Scalars['String'] | null) 83 | type: (Scalars['String'] | null) 84 | link: (Scalars['String'] | null) 85 | additionalLinks: ((Scalars['String'] | null)[] | null) 86 | portal: (Portal | null) 87 | tags: ((Scalars['String'] | null)[] | null) 88 | language: (Scalars['String'] | null) 89 | date: (Scalars['String'] | null) 90 | __typename: 'Post' 91 | } 92 | 93 | export interface Portal { 94 | link: (Scalars['String'] | null) 95 | name: (Scalars['String'] | null) 96 | __typename: 'Portal' 97 | } 98 | 99 | export interface VideoQuery { 100 | totalCount: Scalars['Int'] 101 | retrieved: Scalars['Int'] 102 | processedIn: Scalars['Int'] 103 | videos: ((Video | null)[] | null) 104 | __typename: 'VideoQuery' 105 | } 106 | 107 | export interface Video { 108 | _id: (Scalars['ID'] | null) 109 | title: (Scalars['String'] | null) 110 | abstract: (Scalars['String'] | null) 111 | type: (Scalars['String'] | null) 112 | link: (Scalars['String'] | null) 113 | additionalLinks: ((Scalars['String'] | null)[] | null) 114 | tags: ((Scalars['String'] | null)[] | null) 115 | language: (Scalars['String'] | null) 116 | date: (Scalars['String'] | null) 117 | __typename: 'Video' 118 | } 119 | 120 | export interface ProjectQuery { 121 | totalCount: Scalars['Int'] 122 | retrieved: Scalars['Int'] 123 | processedIn: Scalars['Int'] 124 | projects: ((Project | null)[] | null) 125 | __typename: 'ProjectQuery' 126 | } 127 | 128 | export interface Project { 129 | _id: (Scalars['ID'] | null) 130 | title: (Scalars['String'] | null) 131 | abstract: (Scalars['String'] | null) 132 | type: (Scalars['String'] | null) 133 | link: (Scalars['String'] | null) 134 | additionalLinks: ((Scalars['String'] | null)[] | null) 135 | tags: ((Scalars['String'] | null)[] | null) 136 | language: (Scalars['String'] | null) 137 | date: (Scalars['String'] | null) 138 | __typename: 'Project' 139 | } 140 | 141 | export interface QueryGenqlSelection{ 142 | getTalks?: (TalkQueryGenqlSelection & { __args?: {_id?: (Scalars['ID'] | null), title?: (Scalars['String'] | null), language?: (Scalars['String'] | null), city?: (Scalars['String'] | null), country?: (Scalars['String'] | null), skip?: (Scalars['Int'] | null), limit?: (Scalars['Int'] | null)} }) 143 | isAlive?: boolean | number 144 | getPosts?: (PostQueryGenqlSelection & { __args?: {_id?: (Scalars['ID'] | null), title?: (Scalars['String'] | null), language?: (Scalars['String'] | null), portal?: (Scalars['String'] | null), skip?: (Scalars['Int'] | null), limit?: (Scalars['Int'] | null)} }) 145 | getVideos?: (VideoQueryGenqlSelection & { __args?: {_id?: (Scalars['ID'] | null), title?: (Scalars['String'] | null), language?: (Scalars['String'] | null), skip?: (Scalars['Int'] | null), limit?: (Scalars['Int'] | null)} }) 146 | getProjects?: (ProjectQueryGenqlSelection & { __args?: {_id?: (Scalars['ID'] | null), title?: (Scalars['String'] | null), language?: (Scalars['String'] | null), skip?: (Scalars['Int'] | null), limit?: (Scalars['Int'] | null)} }) 147 | __typename?: boolean | number 148 | __scalar?: boolean | number 149 | } 150 | 151 | export interface TalkQueryGenqlSelection{ 152 | totalCount?: boolean | number 153 | retrieved?: boolean | number 154 | processedIn?: boolean | number 155 | talks?: TalkGenqlSelection 156 | __typename?: boolean | number 157 | __scalar?: boolean | number 158 | } 159 | 160 | export interface TalkGenqlSelection{ 161 | _id?: boolean | number 162 | title?: boolean | number 163 | abstract?: boolean | number 164 | type?: boolean | number 165 | event?: EventGenqlSelection 166 | slides?: boolean | number 167 | images?: ImageGenqlSelection 168 | video?: boolean | number 169 | tags?: boolean | number 170 | location?: LocationGenqlSelection 171 | additionalLinks?: boolean | number 172 | language?: boolean | number 173 | date?: boolean | number 174 | __typename?: boolean | number 175 | __scalar?: boolean | number 176 | } 177 | 178 | export interface EventGenqlSelection{ 179 | link?: boolean | number 180 | name?: boolean | number 181 | __typename?: boolean | number 182 | __scalar?: boolean | number 183 | } 184 | 185 | export interface ImageGenqlSelection{ 186 | url?: boolean | number 187 | filename?: boolean | number 188 | size?: boolean | number 189 | pathId?: boolean | number 190 | __typename?: boolean | number 191 | __scalar?: boolean | number 192 | } 193 | 194 | export interface LocationGenqlSelection{ 195 | latitude?: boolean | number 196 | longitude?: boolean | number 197 | country?: boolean | number 198 | uf?: boolean | number 199 | city?: boolean | number 200 | __typename?: boolean | number 201 | __scalar?: boolean | number 202 | } 203 | 204 | export interface PostQueryGenqlSelection{ 205 | totalCount?: boolean | number 206 | retrieved?: boolean | number 207 | processedIn?: boolean | number 208 | posts?: PostGenqlSelection 209 | __typename?: boolean | number 210 | __scalar?: boolean | number 211 | } 212 | 213 | export interface PostGenqlSelection{ 214 | _id?: boolean | number 215 | title?: boolean | number 216 | abstract?: boolean | number 217 | type?: boolean | number 218 | link?: boolean | number 219 | additionalLinks?: boolean | number 220 | portal?: PortalGenqlSelection 221 | tags?: boolean | number 222 | language?: boolean | number 223 | date?: boolean | number 224 | __typename?: boolean | number 225 | __scalar?: boolean | number 226 | } 227 | 228 | export interface PortalGenqlSelection{ 229 | link?: boolean | number 230 | name?: boolean | number 231 | __typename?: boolean | number 232 | __scalar?: boolean | number 233 | } 234 | 235 | export interface VideoQueryGenqlSelection{ 236 | totalCount?: boolean | number 237 | retrieved?: boolean | number 238 | processedIn?: boolean | number 239 | videos?: VideoGenqlSelection 240 | __typename?: boolean | number 241 | __scalar?: boolean | number 242 | } 243 | 244 | export interface VideoGenqlSelection{ 245 | _id?: boolean | number 246 | title?: boolean | number 247 | abstract?: boolean | number 248 | type?: boolean | number 249 | link?: boolean | number 250 | additionalLinks?: boolean | number 251 | tags?: boolean | number 252 | language?: boolean | number 253 | date?: boolean | number 254 | __typename?: boolean | number 255 | __scalar?: boolean | number 256 | } 257 | 258 | export interface ProjectQueryGenqlSelection{ 259 | totalCount?: boolean | number 260 | retrieved?: boolean | number 261 | processedIn?: boolean | number 262 | projects?: ProjectGenqlSelection 263 | __typename?: boolean | number 264 | __scalar?: boolean | number 265 | } 266 | 267 | export interface ProjectGenqlSelection{ 268 | _id?: boolean | number 269 | title?: boolean | number 270 | abstract?: boolean | number 271 | type?: boolean | number 272 | link?: boolean | number 273 | additionalLinks?: boolean | number 274 | tags?: boolean | number 275 | language?: boolean | number 276 | date?: boolean | number 277 | __typename?: boolean | number 278 | __scalar?: boolean | number 279 | } 280 | 281 | 282 | const Query_possibleTypes: string[] = ['Query'] 283 | export const isQuery = (obj?: { __typename?: any } | null): obj is Query => { 284 | if (!obj?.__typename) throw new Error('__typename is missing in "isQuery"') 285 | return Query_possibleTypes.includes(obj.__typename) 286 | } 287 | 288 | 289 | 290 | const TalkQuery_possibleTypes: string[] = ['TalkQuery'] 291 | export const isTalkQuery = (obj?: { __typename?: any } | null): obj is TalkQuery => { 292 | if (!obj?.__typename) throw new Error('__typename is missing in "isTalkQuery"') 293 | return TalkQuery_possibleTypes.includes(obj.__typename) 294 | } 295 | 296 | 297 | 298 | const Talk_possibleTypes: string[] = ['Talk'] 299 | export const isTalk = (obj?: { __typename?: any } | null): obj is Talk => { 300 | if (!obj?.__typename) throw new Error('__typename is missing in "isTalk"') 301 | return Talk_possibleTypes.includes(obj.__typename) 302 | } 303 | 304 | 305 | 306 | const Event_possibleTypes: string[] = ['Event'] 307 | export const isEvent = (obj?: { __typename?: any } | null): obj is Event => { 308 | if (!obj?.__typename) throw new Error('__typename is missing in "isEvent"') 309 | return Event_possibleTypes.includes(obj.__typename) 310 | } 311 | 312 | 313 | 314 | const Image_possibleTypes: string[] = ['Image'] 315 | export const isImage = (obj?: { __typename?: any } | null): obj is Image => { 316 | if (!obj?.__typename) throw new Error('__typename is missing in "isImage"') 317 | return Image_possibleTypes.includes(obj.__typename) 318 | } 319 | 320 | 321 | 322 | const Location_possibleTypes: string[] = ['Location'] 323 | export const isLocation = (obj?: { __typename?: any } | null): obj is Location => { 324 | if (!obj?.__typename) throw new Error('__typename is missing in "isLocation"') 325 | return Location_possibleTypes.includes(obj.__typename) 326 | } 327 | 328 | 329 | 330 | const PostQuery_possibleTypes: string[] = ['PostQuery'] 331 | export const isPostQuery = (obj?: { __typename?: any } | null): obj is PostQuery => { 332 | if (!obj?.__typename) throw new Error('__typename is missing in "isPostQuery"') 333 | return PostQuery_possibleTypes.includes(obj.__typename) 334 | } 335 | 336 | 337 | 338 | const Post_possibleTypes: string[] = ['Post'] 339 | export const isPost = (obj?: { __typename?: any } | null): obj is Post => { 340 | if (!obj?.__typename) throw new Error('__typename is missing in "isPost"') 341 | return Post_possibleTypes.includes(obj.__typename) 342 | } 343 | 344 | 345 | 346 | const Portal_possibleTypes: string[] = ['Portal'] 347 | export const isPortal = (obj?: { __typename?: any } | null): obj is Portal => { 348 | if (!obj?.__typename) throw new Error('__typename is missing in "isPortal"') 349 | return Portal_possibleTypes.includes(obj.__typename) 350 | } 351 | 352 | 353 | 354 | const VideoQuery_possibleTypes: string[] = ['VideoQuery'] 355 | export const isVideoQuery = (obj?: { __typename?: any } | null): obj is VideoQuery => { 356 | if (!obj?.__typename) throw new Error('__typename is missing in "isVideoQuery"') 357 | return VideoQuery_possibleTypes.includes(obj.__typename) 358 | } 359 | 360 | 361 | 362 | const Video_possibleTypes: string[] = ['Video'] 363 | export const isVideo = (obj?: { __typename?: any } | null): obj is Video => { 364 | if (!obj?.__typename) throw new Error('__typename is missing in "isVideo"') 365 | return Video_possibleTypes.includes(obj.__typename) 366 | } 367 | 368 | 369 | 370 | const ProjectQuery_possibleTypes: string[] = ['ProjectQuery'] 371 | export const isProjectQuery = (obj?: { __typename?: any } | null): obj is ProjectQuery => { 372 | if (!obj?.__typename) throw new Error('__typename is missing in "isProjectQuery"') 373 | return ProjectQuery_possibleTypes.includes(obj.__typename) 374 | } 375 | 376 | 377 | 378 | const Project_possibleTypes: string[] = ['Project'] 379 | export const isProject = (obj?: { __typename?: any } | null): obj is Project => { 380 | if (!obj?.__typename) throw new Error('__typename is missing in "isProject"') 381 | return Project_possibleTypes.includes(obj.__typename) 382 | } 383 | --------------------------------------------------------------------------------