Napiszmy MCP na dwa sposoby w C# i użyjmy go w VS Code, Claude Desktop i Cursor

Cezary Walenciuk

Napiszmy MCP
na dwa sposoby w C# i użyjmy go w
VS Code, Claude Desktop i Cursor

@walenciukC

Speaker
  1. Jaki jest cel tej prezentacji?
  1. Co jeszcze
  1. Co będzie robił nasz MCP
  1. Co będzie robił nasz MCP
blog
CW
Co to jest Model Text Protcol (MCP)🔎?
  1. Co to jest według metafor?
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
  1. Dlaczego MCP to rewolucja
  1. Klasyczny przykład MCP to...
  1. Klasyczny przykład MCP to...
blog
CW
Okej, a jak to działa?
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
Są już programy do testowania swoich MCP
blog
CW
MCP może być konteneryzowane
blog
CW
Napiszmy swój serwer MCP
💻
Czego się nauczyłem patrząc na gotowe MCP?
🤔
  1. Za duża ilość MCP to problem
  1. MCP jest dla LLM, a nie dla kodu
Napiszmy prosty MCP
🛠
Zacznijmy od paczki NuGet
📦
blog
CW
Na tym etapie pisałem kod w listopadzie
  1. Co mają zwracać metody MCP dla typu Tool
Serwer MPC można uruchomoć lokalnie jako plik exe, ale ja będę to robił
w dockerze
🐳
Co chcemy aby nasz MCP robił?
  1. Dostajemy na przykład taką instrukcję
    od biznesu
  1. Dostajemy na przykład taką instrukcję
    od biznesu
Zobaczmy jak wygląda baza danych z dokumentami
📂
blog
CW
Warto zaznaczyć, że konsola
przy początkowym uruchomieniu
nie powinna nic zwracać
blog
CW

Projekt i paczki NuGet

                    

                        <Project Sdk="Microsoft.NET.Sdk">

                        <PropertyGroup>
                            <OutputType>Exe</OutputType>
                            <TargetFramework>net8.0</TargetFramework>
                            <ImplicitUsings>enable</ImplicitUsings>
                            <Nullable>enable</Nullable>
                        </PropertyGroup>
                            <ItemGroup>
                                <PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.9" />
                                <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.6" />
                                <PackageReference Include="ModelContextProtocol" Version="0.4.0-preview.1" />
                                <PackageReference Include="SendGrid" Version="9.29.3" />
                                <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="3.0.2" />
                                <PackageReference Include="System.Text.Json" Version="9.0.6" />
                                <PackageReference Include="EPPlus" Version="7.4.2" />
                            </ItemGroup>
                        </Project>

                    
                

CompanyDocumentDatabaseInitializer

                    



















                        




















                        


                        




















                        


                        




















                        


                        
                        




















                        


                        




















                        


                        




















                        


                        













                        using Microsoft.Data.Sqlite;

                        public static class CompanyDocumentDatabaseInitializer
                        {
                            public static async Task InitializeDatabaseAsync(string dbPath)
                            {
                                var directory = Path.GetDirectoryName(dbPath);

                                if (!string.IsNullOrEmpty(directory))
                                {
                                    Directory.CreateDirectory(directory);
                                }

                                using var connection = new SqliteConnection($"Data Source={dbPath}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    CREATE TABLE IF NOT EXISTS Documents (
                                        Id INTEGER PRIMARY KEY AUTOINCREMENT,
                                        PersonId INTEGER NOT NULL,
                                        Pesel TEXT NOT NULL,
                                        DocumentType TEXT NOT NULL,
                                        DocumentNumber TEXT NOT NULL,
                                        Title TEXT NOT NULL,
                                        Description TEXT,
                                        FilePath TEXT,
                                        CreatedAt TEXT NOT NULL,
                                        ModifiedAt TEXT,
                                        Status TEXT NOT NULL,
                                        UNIQUE(DocumentNumber)
                                    );

                                    CREATE INDEX IF NOT EXISTS idx_documents_pesel ON Documents(Pesel);
                                    CREATE INDEX IF NOT EXISTS idx_documents_personid ON Documents(PersonId);
                                    CREATE INDEX IF NOT EXISTS idx_documents_docnumber ON Documents(DocumentNumber);
                                ";
                                await command.ExecuteNonQueryAsync();

                                // Delete all existing data before seeding
                                command.CommandText = "DELETE FROM Documents";
                                await command.ExecuteNonQueryAsync();

                                // Reset autoincrement counter
                                command.CommandText = "DELETE FROM sqlite_sequence WHERE name='Documents'";
                                await command.ExecuteNonQueryAsync();

                                // Always seed sample data
                                await SeedSampleDataAsync(connection);
                            }

                            private static async Task SeedSampleDataAsync(SqliteConnection connection)
                            {
                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                INSERT INTO Documents (PersonId, Pesel, DocumentType, DocumentNumber, Title, Description, FilePath, CreatedAt, Status)
                                VALUES
                                    -- PersonId 1001
                                    (1001, '90010112345', 'Invoice', 'INV-0001', 'Invoice for Services', 'Professional services rendered', '/docs/inv001.pdf', '2024-01-15', 'Active'),
                                    (1001, '90010112345', 'Contract', 'CNT-0001', 'Employment Contract', 'Full-time employment agreement', '/docs/cnt001.pdf', '2024-01-10', 'Active'),
                                    (1001, '90010112345', 'Receipt', 'RCT-0001', 'Office Supplies Receipt', 'Purchased stationery and printer ink', '/docs/rct001.pdf', '2024-02-05', 'Active'),

                                    -- PersonId 1002
                                    (1002, '85050567890', 'Invoice', 'INV-0002', 'Product Purchase Invoice', 'Hardware equipment', '/docs/inv002.pdf', '2024-02-20', 'Active'),
                                    (1002, '85050567890', 'Report', 'RPT-0001', 'Annual Financial Report', 'Year 2023 summary', '/docs/rpt001.pdf', '2024-03-01', 'Active'),
                                    (1002, '85050567890', 'Agreement', 'AGR-0001', 'NDA with Vendor', 'Non-disclosure agreement for project X', '/docs/agr001.pdf', '2024-03-10', 'Active'),
                                    (1002, '85050567890', 'License', 'LIC-0001', 'Software License', 'Annual subscription for DesignPro', '/docs/lic001.pdf', '2024-04-15', 'Active'),

                                    -- PersonId 1003
                                    (1003, '92120398765', 'Certificate', 'CERT-0001', 'Training Certificate', 'Completed professional training', '/docs/cert001.pdf', '2024-03-15', 'Active'),
                                    (1003, '92120398765', 'Application', 'APP-0001', 'Loan Application', 'Personal loan request', '/docs/app001.pdf', '2024-04-01', 'Pending'),
                                    (1003, '92120398765', 'Warranty', 'WRR-0001', 'Product Warranty', '2-year warranty for laptop', '/docs/wrr001.pdf', '2024-05-20', 'Active'),

                                    -- PersonId 1004
                                    (1004, '88071545678', 'Invoice', 'INV-0003', 'Consulting Invoice', 'IT consulting services', '/docs/inv003.pdf', '2024-05-10', 'Active'),
                                    (1004, '88071545678', 'Proposal', 'PRO-0001', 'Project Proposal', 'Proposal for system upgrade', '/docs/pro001.pdf', '2024-06-05', 'Draft'),
                                    (1004, '88071545678', 'Certificate', 'CERT-0002', 'Safety Training', 'Workplace safety certification', '/docs/cert002.pdf', '2024-06-15', 'Active'),
                                    (1004, '88071545678', 'Receipt', 'RCT-0002', 'Travel Expenses', 'Hotel and transport receipts', '/docs/rct002.pdf', '2024-07-01', 'Active'),

                                    -- PersonId 1005
                                    (1005, '95032012345', 'Contract', 'CNT-0002', 'Freelance Contract', '6-month freelance agreement', '/docs/cnt002.pdf', '2024-07-10', 'Active'),
                                    (1005, '95032012345', 'Invoice', 'INV-0004', 'Freelance Invoice', 'Monthly services invoice', '/docs/inv004.pdf', '2024-08-01', 'Active'),
                                    (1005, '95032012345', 'Report', 'RPT-0002', 'Quarterly Report', 'Q2 financial performance', '/docs/rpt002.pdf', '2024-08-15', 'Active'),
                                    (1005, '95032012345', 'Agreement', 'AGR-0002', 'Partnership Agreement', 'Joint venture with Company Y', '/docs/agr002.pdf', '2024-09-01', 'Active'),

                                    -- PersonId 1006
                                    (1006, '83112578901', 'Certificate', 'CERT-0003', 'First Aid Certificate', 'First aid training completion', '/docs/cert003.pdf', '2024-09-10', 'Active'),
                                    (1006, '83112578901', 'Invoice', 'INV-0005', 'Maintenance Invoice', 'Office maintenance services', '/docs/inv005.pdf', '2024-10-05', 'Active'),
                                    (1006, '83112578901', 'Receipt', 'RCT-0003', 'Office Lunch', 'Team lunch receipt', '/docs/rct003.pdf', '2024-10-15', 'Active'),
                                    (1006, '83112578901', 'License', 'LIC-0002', 'Cloud Storage License', 'Enterprise cloud storage plan', '/docs/lic002.pdf', '2024-11-01', 'Active'),

                                    -- PersonId 1007
                                    (1007, '91041834567', 'Application', 'APP-0002', 'Grant Application', 'Research grant proposal', '/docs/app002.pdf', '2024-11-10', 'Pending'),
                                    (1007, '91041834567', 'Report', 'RPT-0003', 'Research Report', 'Annual research findings', '/docs/rpt003.pdf', '2024-12-01', 'Active'),
                                    (1007, '91041834567', 'Warranty', 'WRR-0002', 'Equipment Warranty', '3-year warranty for lab equipment', '/docs/wrr002.pdf', '2024-12-15', 'Active'),

                                    -- PersonId 1008
                                    (1008, '86093078912', 'Proposal', 'PRO-0002', 'Marketing Proposal', 'Digital marketing campaign plan', '/docs/pro002.pdf', '2025-01-10', 'Draft'),
                                    (1008, '86093078912', 'Invoice', 'INV-0006', 'Advertising Invoice', 'Online ad campaign invoice', '/docs/inv006.pdf', '2025-01-20', 'Active'),
                                    (1008, '86093078912', 'Certificate', 'CERT-0004', 'Marketing Certification', 'Advanced digital marketing course', '/docs/cert004.pdf', '2025-02-05', 'Active'),

                                    -- PersonId 1009
                                    (1009, '93062245678', 'Contract', 'CNT-0003', 'Service Contract', 'Annual IT support contract', '/docs/cnt003.pdf', '2025-02-15', 'Active'),
                                    (1009, '93062245678', 'Agreement', 'AGR-0003', 'Service Level Agreement', 'SLA for IT services', '/docs/agr003.pdf', '2025-03-01', 'Active'),
                                    (1009, '93062245678', 'Receipt', 'RCT-0004', 'Software Purchase', 'New software licenses', '/docs/rct004.pdf', '2025-03-10', 'Active'),

                                    -- PersonId 1010
                                    (1010, '89081412345', 'Invoice', 'INV-0007', 'Consulting Invoice', 'Business consulting services', '/docs/inv007.pdf', '2025-04-01', 'Active'),
                                    (1010, '89081412345', 'Report', 'RPT-0004', 'Audit Report', 'Annual financial audit', '/docs/rpt004.pdf', '2025-04-15', 'Active'),
                                    (1010, '89081412345', 'License', 'LIC-0003', 'Security Software License', 'Enterprise security suite', '/docs/lic003.pdf', '2025-05-01', 'Active'),

                                    -- PersonId 1011
                                    (1011, '87021965432', 'Diploma', 'DIP-0001', 'University Diploma', 'Bachelor of Science in Computer Science', '/docs/dip001.pdf', '2025-05-15', 'Active'),
                                    (1011, '87021965432', 'Resume', 'RES-0001', 'Professional Resume', 'Updated resume for job applications', '/docs/res001.pdf', '2025-05-20', 'Active'),
                                    (1011, '87021965432', 'Reference', 'REF-0001', 'Employment Reference', 'Reference letter from previous employer', '/docs/ref001.pdf', '2025-05-25', 'Active'),

                                    -- PersonId 1012
                                    (1012, '94091178901', 'Insurance', 'INS-0001', 'Health Insurance Policy', 'Annual health insurance coverage', '/docs/ins001.pdf', '2025-06-01', 'Active'),
                                    (1012, '94091178901', 'Claim', 'CLM-0001', 'Insurance Claim', 'Claim for medical expenses', '/docs/clm001.pdf', '2025-06-10', 'Pending'),
                                    (1012, '94091178901', 'Policy', 'POL-0001', 'Privacy Policy', 'Company privacy policy document', '/docs/pol001.pdf', '2025-06-15', 'Active'),

                                    -- PersonId 1013
                                    (1013, '82102432165', 'Manual', 'MAN-0001', 'User Manual', 'Software user guide', '/docs/man001.pdf', '2025-07-01', 'Active'),
                                    (1013, '82102432165', 'Guide', 'GDE-0001', 'Installation Guide', 'Step-by-step installation instructions', '/docs/gde001.pdf', '2025-07-10', 'Active'),
                                    (1013, '82102432165', 'Form', 'FRM-0001', 'Feedback Form', 'Customer feedback collection form', '/docs/frm001.pdf', '2025-07-15', 'Active'),

                                    -- PersonId 1014
                                    (1014, '96011745678', 'Transcript', 'TRN-0001', 'Academic Transcript', 'Official university transcript', '/docs/trn001.pdf', '2025-08-01', 'Active'),
                                    (1014, '96011745678', 'Letter', 'LTR-0001', 'Recommendation Letter', 'Recommendation for graduate school', '/docs/ltr001.pdf', '2025-08-10', 'Active'),
                                    (1014, '96011745678', 'Statement', 'STM-0001', 'Bank Statement', 'Monthly bank statement', '/docs/stm001.pdf', '2025-08-15', 'Active'),

                                    -- PersonId 1015
                                    (1015, '84053098765', 'Permit', 'PRM-0001', 'Work Permit', 'Temporary work permit document', '/docs/prm001.pdf', '2025-09-01', 'Active'),
                                    (1015, '84053098765', 'Ticket', 'TKT-0001', 'Support Ticket', 'IT support request and resolution', '/docs/tkt001.pdf', '2025-09-10', 'Closed'),
                                    (1015, '84053098765', 'Survey', 'SRV-0001', 'Customer Survey', 'Annual customer satisfaction survey', '/docs/srv001.pdf', '2025-09-15', 'Active'),

                                    -- PersonId 1016
                                    (1016, '90120532165', 'Memo', 'MEM-0001', 'Internal Memo', 'Company-wide policy update memo', '/docs/mem001.pdf', '2025-10-01', 'Active'),
                                    (1016, '90120532165', 'Minutes', 'MIN-0001', 'Meeting Minutes', 'Minutes from board meeting', '/docs/min001.pdf', '2025-10-10', 'Active'),
                                    (1016, '90120532165', 'Newsletter', 'NWS-0001', 'Company Newsletter', 'Monthly internal newsletter', '/docs/nws001.pdf', '2025-10-15', 'Active'),

                                    -- PersonId 1017
                                    (1017, '81031865432', 'Budget', 'BGT-0001', 'Project Budget', 'Budget proposal for new project', '/docs/bgt001.pdf', '2025-11-01', 'Draft'),
                                    (1017, '81031865432', 'Schedule', 'SCH-0001', 'Project Schedule', 'Timeline for project delivery', '/docs/sch001.pdf', '2025-11-10', 'Active'),
                                    (1017, '81031865432', 'Checklist', 'CHK-0001', 'Onboarding Checklist', 'New employee onboarding tasks', '/docs/chk001.pdf', '2025-11-15', 'Active'),

                                    -- PersonId 1018
                                    (1018, '97072298765', 'Presentation', 'PRE-0001', 'Sales Presentation', 'Quarterly sales performance presentation', '/docs/pre001.pdf', '2025-12-01', 'Active'),
                                    (1018, '97072298765', 'Brochure', 'BRH-0001', 'Product Brochure', 'Marketing brochure for new product', '/docs/brh001.pdf', '2025-12-10', 'Active'),
                                    (1018, '97072298765', 'Catalog', 'CTG-0001', 'Product Catalog', 'Updated product catalog for 2026', '/docs/ctg001.pdf', '2025-12-15', 'Active'),

                                    -- PersonId 1019
                                    (1019, '05280129353', 'Report', 'RPT-0005', 'Financial Report Q1', 'Detailed financial report for Q1 2025', '/docs/rpt005.pdf', '2025-01-15', 'Active'),
                                    (1019, '05280129353', 'Report', 'RPT-0006', 'Financial Report Q2', 'Detailed financial report for Q2 2025', '/docs/rpt006.pdf', '2025-04-15', 'Active'),
                                    (1019, '05280129353', 'Report', 'RPT-0007', 'Financial Report Q3', 'Detailed financial report for Q3 2025', '/docs/rpt007.pdf', '2025-07-15', 'Active'),
                                    (1019, '05280129353', 'Report', 'RPT-0008', 'Financial Report Q4', 'Detailed financial report for Q4 2025', '/docs/rpt008.pdf', '2025-10-15', 'Active'),

                                    -- PersonId 1020
                                    (1020, '08261437865', 'Invoice', 'INV-0008', 'Invoice January', 'Invoice for January services', '/docs/inv008.pdf', '2025-01-31', 'Active'),
                                    (1020, '08261437865', 'Invoice', 'INV-0009', 'Invoice February', 'Invoice for February services', '/docs/inv009.pdf', '2025-02-28', 'Active'),
                                    (1020, '08261437865', 'Invoice', 'INV-0010', 'Invoice March', 'Invoice for March services', '/docs/inv010.pdf', '2025-03-31', 'Active'),
                                    (1020, '08261437865', 'Invoice', 'INV-0011', 'Invoice April', 'Invoice for April services', '/docs/inv011.pdf', '2025-04-30', 'Active'),
                                    (1020, '08261437865', 'Invoice', 'INV-0012', 'Invoice May', 'Invoice for May services', '/docs/inv012.pdf', '2025-05-31', 'Active'),
                                    (1020, '08261437865', 'Invoice', 'INV-0013', 'Invoice June', 'Invoice for June services', '/docs/inv013.pdf', '2025-06-30', 'Active'),

                                    -- PersonId 1021
                                    (1021, '99010758267', 'Memo', 'MEM-0002', 'HR Memo', 'HR policy update memo', '/docs/mem002.pdf', '2025-01-10', 'Active'),
                                    (1021, '99010758267', 'Memo', 'MEM-0003', 'Office Memo', 'Office closure announcement', '/docs/mem003.pdf', '2025-05-10', 'Active'),
                                    (1021, '99010758267', 'Memo', 'MEM-0004', 'Safety Memo', 'Workplace safety updates', '/docs/mem004.pdf', '2025-09-10', 'Active'),

                                    -- PersonId 1022
                                    (1022, '66112435932', 'Contract', 'CTR-0001', 'Client Contract A', 'Contract agreement with Client A', '/docs/ctr001.pdf', '2025-02-01', 'Active'),
                                    (1022, '66112435932', 'Contract', 'CTR-0002', 'Client Contract B', 'Contract agreement with Client B', '/docs/ctr002.pdf', '2025-03-01', 'Active'),
                                    (1022, '66112435932', 'Contract', 'CTR-0003', 'Vendor Contract', 'Contract agreement with Vendor X', '/docs/ctr003.pdf', '2025-06-01', 'Active'),

                                    -- PersonId 1023
                                    (1023, '84101192381', 'Policy', 'PLC-0001', 'Privacy Policy', 'Updated company privacy policy', '/docs/plc001.pdf', '2025-01-20', 'Active'),
                                    (1023, '84101192381', 'Policy', 'PLC-0002', 'Security Policy', 'Updated IT security policy', '/docs/plc002.pdf', '2025-04-20', 'Active'),
                                    (1023, '84101192381', 'Policy', 'PLC-0003', 'Leave Policy', 'Employee leave policy update', '/docs/plc003.pdf', '2025-08-20', 'Active'),

                                    -- PersonId 1024
                                    (1024, '70032189288', 'Form', 'FRM-0002', 'Leave Application Form', 'Form for applying employee leave', '/docs/frm002.pdf', '2025-01-05', 'Active'),
                                    (1024, '70032189288', 'Form', 'FRM-0003', 'Expense Form', 'Form for submitting expenses', '/docs/frm003.pdf', '2025-03-05', 'Active'),
                                    (1024, '70032189288', 'Form', 'FRM-0004', 'Feedback Form', 'Customer feedback submission form', '/docs/frm004.pdf', '2025-06-05', 'Active'),

                                    -- PersonId 1025
                                    (1025, '77092655919', 'Letter', 'LTR-0002', 'Offer Letter', 'Job offer letter for new hire', '/docs/ltr002.pdf', '2025-02-15', 'Active'),
                                    (1025, '77092655919', 'Letter', 'LTR-0003', 'Resignation Letter', 'Formal resignation letter', '/docs/ltr003.pdf', '2025-05-15', 'Active'),
                                    (1025, '77092655919', 'Letter', 'LTR-0004', 'Recommendation Letter', 'Letter of recommendation', '/docs/ltr004.pdf', '2025-09-15', 'Active'),

                                    -- PersonId 1026
                                    (1026, '76072625775', 'Manual', 'MNL-0001', 'User Manual', 'User manual for product A', '/docs/mnl001.pdf', '2025-02-28', 'Active'),
                                    (1026, '76072625775', 'Manual', 'MNL-0002', 'Installation Guide', 'Installation manual for product B', '/docs/mnl002.pdf', '2025-06-28', 'Active'),
                                    (1026, '76072625775', 'Manual', 'MNL-0003', 'Training Manual', 'Internal training documentation', '/docs/mnl003.pdf', '2025-10-28', 'Active');
                            ";
                                await command.ExecuteNonQueryAsync();
                            }

                        }
                    
                
blog
CW

CompanyDocument

                    

                        public partial class CompanyDocument
                        {
                            public int Id { get; set; }
                            public int PersonId { get; set; }
                            public string Pesel { get; set; } = string.Empty;
                            public string DocumentType { get; set; } = string.Empty;
                            public string DocumentNumber { get; set; } = string.Empty;
                            public string Title { get; set; } = string.Empty;
                            public string Description { get; set; } = string.Empty;
                            public string FilePath { get; set; } = string.Empty;
                            public DateTime CreatedAt { get; set; }
                            public DateTime? ModifiedAt { get; set; }
                            public string Status { get; set; } = string.Empty;
                        }

                    
                

CompanyDocumentsRepository

                    
























                        





                        




















                        

















                        















                        


                        using MCPServerDocuments.Database;
                        using MCPServerDocuments.DataClasses;
                        using Microsoft.Data.Sqlite;

                        namespace MyMPCServer.Database;

                        public class CompanyDocumentsRepository
                        {
                            private readonly DatabaseConfig dbConfig;

                            public CompanyDocumentsRepository(DatabaseConfig dbConfig)
                            {
                                this.dbConfig = dbConfig;
                            }

                            public async Task<List<CompanyDocument>> GetDocumentsByPeselAsync(string pesel)
                            {
                                var documents = new List<CompanyDocument>();

                                using var connection = new SqliteConnection($"Data Source={dbConfig.ConnectionString}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    SELECT Id, PersonId, Pesel, DocumentType, DocumentNumber, Title, 
                                        Description, FilePath, CreatedAt, ModifiedAt, Status
                                    FROM Documents
                                    WHERE Pesel = $pesel
                                    ORDER BY CreatedAt DESC";
                                command.Parameters.AddWithValue("$pesel", pesel);

                                using var reader = await command.ExecuteReaderAsync();
                                while (await reader.ReadAsync())
                                {
                                    documents.Add(MapDocument(reader));
                                }

                                return documents;
                            }

                            public async Task<List<CompanyDocument>> GetDocumentsByPersonIdAsync(int personId)
                            {
                                var documents = new List<CompanyDocument>();

                                using var connection = new SqliteConnection($"Data Source={dbConfig.ConnectionString}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    SELECT Id, PersonId, Pesel, DocumentType, DocumentNumber, Title, 
                                        Description, FilePath, CreatedAt, ModifiedAt, Status
                                    FROM Documents
                                    WHERE PersonId = $personId
                                    ORDER BY CreatedAt DESC";
                                command.Parameters.AddWithValue("$personId", personId);

                                using var reader = await command.ExecuteReaderAsync();
                                while (await reader.ReadAsync())
                                {
                                    documents.Add(MapDocument(reader));
                                }

                                return documents;
                            }

                            public async Task<int> AddDocumentAsync(CompanyDocument document)
                            {
                                using var connection = new SqliteConnection($"Data Source={dbConfig.ConnectionString}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    INSERT INTO Documents (PersonId, Pesel, DocumentType, DocumentNumber, Title, 
                                                        Description, FilePath, CreatedAt, Status)
                                    VALUES ($personId, $pesel, $docType, $docNumber, $title, $description, 
                                            $filePath, $createdAt, $status);
                                    SELECT last_insert_rowid();";

                                command.Parameters.AddWithValue("$personId", document.PersonId);
                                command.Parameters.AddWithValue("$pesel", document.Pesel);
                                command.Parameters.AddWithValue("$docType", document.DocumentType);
                                command.Parameters.AddWithValue("$docNumber", document.DocumentNumber);
                                command.Parameters.AddWithValue("$title", document.Title);
                                command.Parameters.AddWithValue("$description", document.Description);
                                command.Parameters.AddWithValue("$filePath", document.FilePath);
                                command.Parameters.AddWithValue("$createdAt", document.CreatedAt);
                                command.Parameters.AddWithValue("$status", document.Status);

                                var result = await command.ExecuteScalarAsync();
                                return Convert.ToInt32(result);
                            }

                            private CompanyDocument MapDocument(SqliteDataReader reader)
                            {
                                return new CompanyDocument
                                {
                                    Id = reader.GetInt32(0),
                                    PersonId = reader.GetInt32(1),
                                    Pesel = reader.GetString(2),
                                    DocumentType = reader.GetString(3),
                                    DocumentNumber = reader.GetString(4),
                                    Title = reader.GetString(5),
                                    Description = reader.GetString(6),
                                    FilePath = reader.GetString(7),
                                    CreatedAt = reader.GetDateTime(8),
                                    ModifiedAt = reader.IsDBNull(9) ? null : reader.GetDateTime(9),
                                    Status = reader.GetString(10)
                                };
                            }
                        }


                    
                

DatabaseConfig

                    

                        public partial class DatabaseConfig
                        {
                            public string ConnectionString { get; set; } = string.Empty;
                        }


                    
                
Zobaczmy jak wygląda nasz MCPServer napisany w C#

MCPServerDataExportTools

                    

                        



















                        























                        [McpServerToolType]
                        public sealed class MCPServerDataExportTools
                        {
                            private readonly CompanyDocumentsRepository _repository;

                            public MCPServerDataExportTools(
                                CompanyDocumentsRepository repository
                        )
                            {
                                _repository = repository;
                            }

                            [McpServerTool, Description("Search for documents by PESEL number.")]
                            public async Task<string> SearchDocumentsByPesel(
                            [Description("PESEL number to search for")] string pesel)
                            {
                                try
                                {
                                    var documents = await _repository.GetDocumentsByPeselAsync(pesel);

                                    return JsonSerializer.Serialize(new SearchDocumentsByPeselResult
                                    {
                                        Success = true,
                                        Message = $"Found {documents.Count} document(s) for PESEL: {pesel}",
                                        Pesel = pesel,
                                        DocumentCount = documents.Count,
                                        Documents = documents,
                                        Timestamp = DateTime.UtcNow
                                    }, CompanyDocumentJsonContext.Default.SearchDocumentsByPeselResult);
                                }
                                catch (Exception ex)
                                {
                                    return JsonSerializer.Serialize(new
                                    {
                                        success = false,
                                        error = ex.Message,
                                        timestamp = DateTime.UtcNow
                                    }, CompanyDocumentJsonContext.Default.SearchDocumentsError);
                                }
                            }

                            [McpServerTool, Description("Search for documents by Person ID.")]
                            public async Task<string> SearchDocumentsByPersonId(
                                [Description("Person ID to search for")] int personId)
                            {
                                try
                                {
                                    var documents = await _repository.GetDocumentsByPersonIdAsync(personId);

                                    return JsonSerializer.Serialize(new SearchDocumentsByPersonIdResult
                                    {
                                        Success = true,
                                        Message = $"Found {documents.Count} document(s) for Person ID: {personId}",
                                        PersonId = personId,
                                        DocumentCount = documents.Count,
                                        Documents = documents,
                                        Timestamp = DateTime.UtcNow
                                    }, CompanyDocumentJsonContext.Default.SearchDocumentsByPersonIdResult);
                                }
                                catch (Exception ex)
                                {
                                    return JsonSerializer.Serialize(new
                                    {
                                        success = false,
                                        error = ex.Message,
                                        timestamp = DateTime.UtcNow
                                    }, CompanyDocumentJsonContext.Default.SearchDocumentsError);
                                }
                            }
                        }
                                            
                

Klasy do serializacji JSON

                    















                        // JSON serialization context
                        [JsonSerializable(typeof(SearchDocumentsByPeselResult))]
                        [JsonSerializable(typeof(SearchDocumentsError))]
                        [JsonSerializable(typeof(SearchDocumentsByPersonIdResult))]
                        public partial class CompanyDocumentJsonContext : JsonSerializerContext
                        {
                        }


                        public partial class SearchDocumentsByPeselResult
                        {
                            public bool Success { get; set; }
                            public string Message { get; set; } = string.Empty;
                            public string Pesel { get; set; } = string.Empty;
                            public int DocumentCount { get; set; }
                            public List<CompanyDocument> Documents { get; set; } = new();
                            public DateTime Timestamp { get; set; }
                        }

                        public partial class SearchDocumentsByPersonIdResult
                        {
                            public bool Success { get; set; }
                            public string Message { get; set; } = string.Empty;
                            public int PersonId { get; set; }
                            public int DocumentCount { get; set; }
                            public List<CompanyDocument> Documents { get; set; } = new();
                            public DateTime Timestamp { get; set; }
                        }

                        public partial class SearchDocumentsError
                        {
                            public bool Success { get; set; }
                            public string Error { get; set; } = string.Empty;
                            public DateTime Timestamp { get; set; }
                        }
                    
                
Jak wygląda program.cs w naszym MCPServer?

Klasy do serializacji JSON

                    













                        using MCPServerDocuments;
                        using MCPServerDocuments.Database;
                        using Microsoft.Extensions.DependencyInjection;
                        using Microsoft.Extensions.Hosting;
                        using MyMPCServer.Database;
                        using System.Text.Json.Serialization;

                        var builder = Host.CreateEmptyApplicationBuilder(settings: null);

                        builder.Services
                            .AddMcpServer()
                            .WithStdioServerTransport()
                            .WithTools<MCPServerDataExportTools>();

                        builder.Services.AddSingleton<CompanyDocumentsRepository>();

                        var defaultDbPath = Path.Combine(
                            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), 
                            "MCPServerDocuments", 
                            "documents.db" 
                        );

                        builder.Services.AddSingleton<DatabaseConfig>(provider => new DatabaseConfig
                        {
                            ConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING") ?? defaultDbPath
                        });

                        var dbConfig = builder.Services.BuildServiceProvider().GetRequiredService<DatabaseConfig>();

                        await CompanyDocumentDatabaseInitializer.InitializeDatabaseAsync(dbConfig.ConnectionString);

                        await builder.Build().RunAsync();
                    
                
blog
CW
Konteneryzacja naszego MCP Server w dockerze
Jak wyglądają pliki dockera?

Docker

                    

                        # Use the official .NET SDK image for building
                        FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
                        WORKDIR /src

                        # Copy project files
                        COPY *.csproj ./
                        RUN dotnet restore

                        # Copy remaining source code
                        COPY . ./
                        # Publish as self-contained executable for linux-x64
                        RUN dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -o /app/publish

                        # Use minimal runtime image since we have self-contained exe
                        FROM mcr.microsoft.com/dotnet/runtime-deps:8.0
                        WORKDIR /app

                        # Copy published app
                        COPY --from=build /app/publish .

                        # Create directory for database
                        RUN mkdir -p /data

                        # Set environment variable for database path
                        ENV DB_CONNECTION_STRING=/data/documents.db

                        # Run the self-contained executable
                        ENTRYPOINT ["./MCPServerDocuments"]
                    
                

.dockerignore

                    
                        bin/
                        obj/
                        *.db
                        *.db-shm
                        *.db-wal
                        data/
                        .vs/
                        .vscode/
                        *.user
                        .git/
                        .gitignore
                        README.md
                        Dockerfile
                        docker-compose.yml
                    
                

Polecenia docker

                    

                        cd "to project directory"

                        docker build -t mcp-server-documents:latest .

                        docker run --rm -i -v ${PWD}/data:/data mcp-server-documents:latest
                    
                
Testowanie w MCPInspectorze
blog
CW

Polecenia docker

                    
                        cd "to MCPInspector directory"
                        npx @modelcontextprotocol/inspector dotnet run "MCPServerDocuments. csproj"
                    
                
blog
CW
blog
CW
  1. W sumie co testuje MCP Inspector
Dobrze jest napisać testy jednostkowe dla metod
Zobaczmy jak można podłączyć się
do naszego MCPServer z...
blog
CW
Claude Desktop
blog
CW
blog
CW

mcpServers Claude Desktop

                    





                        {
                            "mcpServers": {
                                "MCP_DOCKER": {
                                    "command": "docker",
                                    "args": ["mcp", "gateway", "run"],
                                    "env": {
                                        "LOCALAPPDATA": "C:\\Users\\Cezary\\AppData\\Local",
                                        "ProgramData": "C:\\ProgramData",
                                        "ProgramFiles": "C:\\Program Files"
                                    }
                                },
                                "company-documents": {
                                    "command": "docker",
                                    "args": [
                                        "run",
                                        "--rm",
                                        "-i",
                                        "-v",
                                        "C:\\Users\\Cezary\\mcp-documents-data:/data",
                                        "mcp-server-documents:latest"
                                    ]
                                }
                            }
                        }
                    
                
blog
CW
blog
CW
blog
CW
blog
CW
Cursor
blog
CW
blog
CW

Cursor json

                    






















                        {
                        "mcpServers": {
                            "obsidian": {
                            "command": "docker",
                            "args": [
                                "run",
                                "-i",
                                "--rm",
                                "-e",
                                "OBSIDIAN_HOST",
                                "-e",
                                "OBSIDIAN_API_KEY",
                                "mcp/obsidian"
                            ],
                            "env": {
                                "OBSIDIAN_HOST": "host.docker.internal",
                                "OBSIDIAN_API_KEY": ""
                            }
                            },
                            "youtube_transcript": {
                            "command": "docker",
                            "args": [
                                "run",
                                "-i",
                                "--rm",
                                "mcp/youtube-transcript"
                            ]
                            },
                            "company-documents": {
                            "command": "docker",
                            "args": [
                                "run",
                                "-i",
                                "--rm",
                                "-v",
                                "C:\\Users\\Cezary\\mcp-documents-data:/data",
                                "mcp-server-documents:latest"
                            ]
                            }
                        }
                    }
                    
                
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
LLM Studio
blog
CW
blog
CW
blog
CW

LLM Studio json

                    








                        {
                        "mcpServers": {
                            "MCP_DOCKER": {
                            "command": "docker",
                            "args": [
                                "mcp",
                                "gateway",
                                "run"
                            ],
                            "env": {
                                "LOCALAPPDATA": "C:\\Users\\Cezary\\AppData\\Local",
                                "ProgramData": "C:\\ProgramData",
                                "ProgramFiles": "C:\\Program Files"
                            }
                            },
                            "company-documents": {
                            "command": "docker",
                            "args": [
                                "run",
                                "--rm",
                                "-i",
                                "-v",
                                "C:\\Users\\Cezary\\mcp-documents-data:/data",
                                "mcp-server-documents:latest"
                            ]
                            }
                        }
                        }
                    
                
blog
CW
blog
CW
blog
CW
VS Code i CoPilot
No to szukamy
blog
CW
blog
CW
blog
CW
Nie tędy droga... Visual Studio Code z CoPilotem
blog
CW
blog
CW

Polecenia docker

                    

                        {
                        "servers": {
                            "github": {
                            "url": "https://api.githubcopilot.com/mcp/"
                            },
                            "company-documents": {
                            "command": "docker",
                            "args": [
                                "run",
                                "-i",
                                "--rm",
                                "-v",
                                "C:\\Users\\Cezary\\mcp-documents-data:/data",
                                "mcp-server-documents:latest"
                            ]
                            }
                        },
                        "inputs": []
                        }
                    
                
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
VS Code i .Continue
blog
CW
blog
CW
O nie!
Za mało ramu na laptopie!
blog
CW
Nie działa...
Nie widzi mojego serwera MCP
blog
CW
blog
CW

YAML główna konfiguracja Continue

                    
                        name: Local Agent
                        version: 1.0.0
                        schema: v1
                        models:
                        - name: Autodetect
                            provider: ollama
                            model: AUTODETECT
                        mcpServers:
                        - name: company-documents
                            command: docker
                            args:
                            - run
                            - -i
                            - --rm
                            - -v
                            - C:\Users\Cezary\mcp-documents-data:/data
                            - mcp-server-documents:latest
                            env: {}

                    
                
Nadal nie działa...
Nie widzi mojego serwera MCP
blog
CW
Po restarcie laptopa...
zaczął on widzieć mój serwer MCP
blog
CW
Teraz gdy znamy podstawy możemy rozbudować nasz MCPServer
o kolejne funkcjonalności
  1. Dostajemy na przykład taką instrukcję
    od biznesu
  1. Dostajemy na przykład taką instrukcję
    od biznesu
Jak wygląda historia użytkowników w naszej bazie?

UserHistoryRepository

                    



































































































































































































































































































































                        using MCPServerDocuments.Database;
                        using MCPServerDocuments.DataClasses;
                        using Microsoft.Data.Sqlite;

                        namespace MyMPCServer.Database;

                        public class UserHistoryRepository
                        {
                            private readonly DatabaseConfig dbConfig;

                            public UserHistoryRepository(DatabaseConfig dbConfig)
                            {
                                this.dbConfig = dbConfig;
                            }

                            #region User Methods

                            public async Task<User?> GetUserByIdAsync(int userId)
                            {
                                using var connection = new SqliteConnection($"Data Source={dbConfig.ConnectionString}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    SELECT Id, UserName
                                    FROM Users
                                    WHERE Id = $userId";
                                command.Parameters.AddWithValue("$userId", userId);

                                using var reader = await command.ExecuteReaderAsync();
                                if (await reader.ReadAsync())
                                {
                                    return MapUser(reader);
                                }

                                return null;
                            }

                            public async Task<User?> GetUserByNameAsync(string userName)
                            {
                                using var connection = new SqliteConnection($"Data Source={dbConfig.ConnectionString}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    SELECT Id, UserName
                                    FROM Users
                                    WHERE UserName = $userName";
                                command.Parameters.AddWithValue("$userName", userName);

                                using var reader = await command.ExecuteReaderAsync();
                                if (await reader.ReadAsync())
                                {
                                    return MapUser(reader);
                                }

                                return null;
                            }

                            public async Task<List<User>> GetAllUsersAsync()
                            {
                                var users = new List<User>();

                                using var connection = new SqliteConnection($"Data Source={dbConfig.ConnectionString}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    SELECT Id, UserName
                                    FROM Users
                                    ORDER BY UserName";

                                using var reader = await command.ExecuteReaderAsync();
                                while (await reader.ReadAsync())
                                {
                                    users.Add(MapUser(reader));
                                }

                                return users;
                            }

                            public async Task<int> AddUserAsync(User user)
                            {
                                using var connection = new SqliteConnection($"Data Source={dbConfig.ConnectionString}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    INSERT INTO Users (UserName)
                                    VALUES ($userName);
                                    SELECT last_insert_rowid();";

                                command.Parameters.AddWithValue("$userName", user.UserName);

                                var result = await command.ExecuteScalarAsync();
                                return Convert.ToInt32(result);
                            }

                            #endregion

                            #region User History Methods

                            public async Task<List<UserHistoryUsingCompanyDocuments>> GetHistoryByUserIdAsync(int userId)
                            {
                                var history = new List<UserHistoryUsingCompanyDocuments>();

                                using var connection = new SqliteConnection($"Data Source={dbConfig.ConnectionString}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    SELECT Id, UserId, UserQuery, Action, DocumentId, CreatedAt
                                    FROM UserHistoryUsingCompanyDocuments
                                    WHERE UserId = $userId
                                    ORDER BY CreatedAt DESC";
                                command.Parameters.AddWithValue("$userId", userId);

                                using var reader = await command.ExecuteReaderAsync();
                                while (await reader.ReadAsync())
                                {
                                    history.Add(MapUserHistory(reader));
                                }

                                return history;
                            }

                            public async Task<List<UserHistoryUsingCompanyDocuments>> GetHistoryByDocumentIdAsync(int documentId)
                            {
                                var history = new List<UserHistoryUsingCompanyDocuments>();

                                using var connection = new SqliteConnection($"Data Source={dbConfig.ConnectionString}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    SELECT Id, UserId, UserQuery, Action, DocumentId, CreatedAt
                                    FROM UserHistoryUsingCompanyDocuments
                                    WHERE DocumentId = $documentId
                                    ORDER BY CreatedAt DESC";
                                command.Parameters.AddWithValue("$documentId", documentId);

                                using var reader = await command.ExecuteReaderAsync();
                                while (await reader.ReadAsync())
                                {
                                    history.Add(MapUserHistory(reader));
                                }

                                return history;
                            }

                            public async Task<List<UserHistoryUsingCompanyDocuments>> GetHistoryByActionAsync(string action)
                            {
                                var history = new List<UserHistoryUsingCompanyDocuments>();

                                using var connection = new SqliteConnection($"Data Source={dbConfig.ConnectionString}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    SELECT Id, UserId, UserQuery, Action, DocumentId, CreatedAt
                                    FROM UserHistoryUsingCompanyDocuments
                                    WHERE Action = $action
                                    ORDER BY CreatedAt DESC";
                                command.Parameters.AddWithValue("$action", action);

                                using var reader = await command.ExecuteReaderAsync();
                                while (await reader.ReadAsync())
                                {
                                    history.Add(MapUserHistory(reader));
                                }

                                return history;
                            }

                            public async Task<int> AddHistoryAsync(UserHistoryUsingCompanyDocuments history)
                            {
                                using var connection = new SqliteConnection($"Data Source={dbConfig.ConnectionString}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    INSERT INTO UserHistoryUsingCompanyDocuments (UserId, UserQuery, Action, DocumentId, CreatedAt)
                                    VALUES ($userId, $userQuery, $action, $documentId, $createdAt);
                                    SELECT last_insert_rowid();";

                                command.Parameters.AddWithValue("$userId", history.UserId);
                                command.Parameters.AddWithValue("$userQuery", history.UserQuery);
                                command.Parameters.AddWithValue("$action", history.Action);
                                command.Parameters.AddWithValue("$documentId", history.DocumentId);
                                command.Parameters.AddWithValue("$createdAt", history.CreatedAt);

                                var result = await command.ExecuteScalarAsync();
                                return Convert.ToInt32(result);
                            }

                            #endregion

                            #region Joined Query Methods

                            public async Task<List<UserHistoryWithDetails>> GetHistoryWithDetailsByUserIdAsync(int userId)
                            {
                                var history = new List<UserHistoryWithDetails>();

                                using var connection = new SqliteConnection($"Data Source={dbConfig.ConnectionString}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    SELECT 
                                        h.Id as HistoryId,
                                        h.UserId,
                                        u.UserName,
                                        h.UserQuery,
                                        h.Action,
                                        h.DocumentId,
                                        d.Title as DocumentTitle,
                                        d.DocumentType,
                                        d.DocumentNumber,
                                        h.CreatedAt
                                    FROM UserHistoryUsingCompanyDocuments h
                                    INNER JOIN Users u ON h.UserId = u.Id
                                    INNER JOIN Documents d ON h.DocumentId = d.Id
                                    WHERE h.UserId = $userId
                                    ORDER BY h.CreatedAt DESC";
                                command.Parameters.AddWithValue("$userId", userId);

                                using var reader = await command.ExecuteReaderAsync();
                                while (await reader.ReadAsync())
                                {
                                    history.Add(MapUserHistoryWithDetails(reader));
                                }

                                return history;
                            }

                            public async Task<List<UserHistoryWithDetails>> GetHistoryWithDetailsByDocumentIdAsync(int documentId)
                            {
                                var history = new List<UserHistoryWithDetails>();

                                using var connection = new SqliteConnection($"Data Source={dbConfig.ConnectionString}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    SELECT 
                                        h.Id as HistoryId,
                                        h.UserId,
                                        u.UserName,
                                        h.UserQuery,
                                        h.Action,
                                        h.DocumentId,
                                        d.Title as DocumentTitle,
                                        d.DocumentType,
                                        d.DocumentNumber,
                                        h.CreatedAt
                                    FROM UserHistoryUsingCompanyDocuments h
                                    INNER JOIN Users u ON h.UserId = u.Id
                                    INNER JOIN Documents d ON h.DocumentId = d.Id
                                    WHERE h.DocumentId = $documentId
                                    ORDER BY h.CreatedAt DESC";
                                command.Parameters.AddWithValue("$documentId", documentId);

                                using var reader = await command.ExecuteReaderAsync();
                                while (await reader.ReadAsync())
                                {
                                    history.Add(MapUserHistoryWithDetails(reader));
                                }

                                return history;
                            }

                            public async Task<List<UserHistoryWithDetails>> GetAllHistoryWithDetailsAsync()
                            {
                                var history = new List<UserHistoryWithDetails>();

                                using var connection = new SqliteConnection($"Data Source={dbConfig.ConnectionString}");
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    SELECT 
                                        h.Id as HistoryId,
                                        h.UserId,
                                        u.UserName,
                                        h.UserQuery,
                                        h.Action,
                                        h.DocumentId,
                                        d.Title as DocumentTitle,
                                        d.DocumentType,
                                        d.DocumentNumber,
                                        h.CreatedAt
                                    FROM UserHistoryUsingCompanyDocuments h
                                    INNER JOIN Users u ON h.UserId = u.Id
                                    INNER JOIN Documents d ON h.DocumentId = d.Id
                                    ORDER BY h.CreatedAt DESC";

                                using var reader = await command.ExecuteReaderAsync();
                                while (await reader.ReadAsync())
                                {
                                    history.Add(MapUserHistoryWithDetails(reader));
                                }

                                return history;
                            }

                            #endregion

                            #region Mapping Methods

                            private User MapUser(SqliteDataReader reader)
                            {
                                return new User
                                {
                                    Id = reader.GetInt32(0),
                                    UserName = reader.GetString(1)
                                };
                            }

                            private UserHistoryUsingCompanyDocuments MapUserHistory(SqliteDataReader reader)
                            {
                                return new UserHistoryUsingCompanyDocuments
                                {
                                    Id = reader.GetInt32(0),
                                    UserId = reader.GetInt32(1),
                                    UserQuery = reader.GetString(2),
                                    Action = reader.GetString(3),
                                    DocumentId = reader.GetInt32(4),
                                    CreatedAt = reader.GetDateTime(5)
                                };
                            }

                            private UserHistoryWithDetails MapUserHistoryWithDetails(SqliteDataReader reader)
                            {
                                return new UserHistoryWithDetails
                                {
                                    HistoryId = reader.GetInt32(0),
                                    UserId = reader.GetInt32(1),
                                    UserName = reader.GetString(2),
                                    UserQuery = reader.GetString(3),
                                    Action = reader.GetString(4),
                                    DocumentId = reader.GetInt32(5),
                                    DocumentTitle = reader.GetString(6),
                                    DocumentType = reader.GetString(7),
                                    DocumentNumber = reader.GetString(8),
                                    CreatedAt = reader.GetDateTime(9)
                                };
                            }

                            #endregion
                        }

                    
                

DatabaseInitializer

                    












































































































































































































































































                        using Microsoft.Data.Sqlite;

                        namespace MCPServerDocuments.Database;

                        public static class DatabaseInitializer
                        {
                            public static async Task InitializeDatabaseAsync(string dbPath)
                            {
                                var directory = Path.GetDirectoryName(dbPath);
                                if (!string.IsNullOrEmpty(directory))
                                {
                                    Directory.CreateDirectory(directory);
                                }

                                using var connection = new SqliteConnection($"Data Source={dbPath}");
                                await connection.OpenAsync();

                                // Create all tables
                                await CreateTablesAsync(connection);

                                // Clear existing data
                                await ClearDataAsync(connection);

                                // Seed sample data
                                await SeedSampleDataAsync(connection);
                            }

                            private static async Task CreateTablesAsync(SqliteConnection connection)
                            {
                                var command = connection.CreateCommand();
                                command.CommandText = @"
                                    CREATE TABLE IF NOT EXISTS Users (
                                        Id INTEGER PRIMARY KEY AUTOINCREMENT,
                                        UserName TEXT NOT NULL UNIQUE
                                    );

                                    CREATE TABLE IF NOT EXISTS Documents (
                                        Id INTEGER PRIMARY KEY AUTOINCREMENT,
                                        PersonId INTEGER NOT NULL,
                                        Pesel TEXT NOT NULL,
                                        DocumentType TEXT NOT NULL,
                                        DocumentNumber TEXT NOT NULL,
                                        Title TEXT NOT NULL,
                                        Description TEXT,
                                        FilePath TEXT,
                                        CreatedAt TEXT NOT NULL,
                                        ModifiedAt TEXT,
                                        Status TEXT NOT NULL,
                                        UNIQUE(DocumentNumber)
                                    );

                                    CREATE TABLE IF NOT EXISTS UserHistoryUsingCompanyDocuments (
                                        Id INTEGER PRIMARY KEY AUTOINCREMENT,
                                        UserId INTEGER NOT NULL,
                                        UserQuery TEXT NOT NULL,
                                        Action TEXT NOT NULL,
                                        DocumentId INTEGER NOT NULL,
                                        CreatedAt TEXT NOT NULL,
                                        FOREIGN KEY (UserId) REFERENCES Users(Id),
                                        FOREIGN KEY (DocumentId) REFERENCES Documents(Id)
                                    );

                                    CREATE INDEX IF NOT EXISTS idx_users_username ON Users(UserName);
                                    CREATE INDEX IF NOT EXISTS idx_documents_pesel ON Documents(Pesel);
                                    CREATE INDEX IF NOT EXISTS idx_documents_personid ON Documents(PersonId);
                                    CREATE INDEX IF NOT EXISTS idx_documents_docnumber ON Documents(DocumentNumber);
                                    CREATE INDEX IF NOT EXISTS idx_user_history_userid ON UserHistoryUsingCompanyDocuments(UserId);
                                    CREATE INDEX IF NOT EXISTS idx_user_history_documentid ON UserHistoryUsingCompanyDocuments(DocumentId);
                                    CREATE INDEX IF NOT EXISTS idx_user_history_action ON UserHistoryUsingCompanyDocuments(Action);
                                ";
                                await command.ExecuteNonQueryAsync();
                            }

                            private static async Task ClearDataAsync(SqliteConnection connection)
                            {
                                var command = connection.CreateCommand();

                                // Delete data in correct order (respecting foreign keys)
                                command.CommandText = "DELETE FROM UserHistoryUsingCompanyDocuments";
                                await command.ExecuteNonQueryAsync();

                                command.CommandText = "DELETE FROM Users";
                                await command.ExecuteNonQueryAsync();

                                command.CommandText = "DELETE FROM Documents";
                                await command.ExecuteNonQueryAsync();

                                // Reset autoincrement counters
                                command.CommandText = @"
                                    DELETE FROM sqlite_sequence WHERE name IN ('Users', 'Documents', 'UserHistoryUsingCompanyDocuments')
                                ";
                                await command.ExecuteNonQueryAsync();
                            }

                            private static async Task SeedSampleDataAsync(SqliteConnection connection)
                            {
                                var command = connection.CreateCommand();

                                // Insert sample users
                                command.CommandText = @"
                                    INSERT INTO Users (UserName)
                                    VALUES 
                                        ('john.doe'),
                                        ('jane.smith'),
                                        ('admin.user'),
                                        ('bob.johnson'),
                                        ('alice.williams');
                                ";
                                await command.ExecuteNonQueryAsync();

                                // Insert sample documents
                                command.CommandText = @"
                                INSERT INTO Documents (PersonId, Pesel, DocumentType, DocumentNumber, Title, Description, FilePath, CreatedAt, Status)
                                VALUES
                                    -- PersonId 1001
                                    (1001, '90010112345', 'Invoice', 'INV-0001', 'Invoice for Services', 'Professional services rendered', '/docs/inv001.pdf', '2024-01-15', 'Active'),
                                    (1001, '90010112345', 'Contract', 'CNT-0001', 'Employment Contract', 'Full-time employment agreement', '/docs/cnt001.pdf', '2024-01-10', 'Active'),
                                    (1001, '90010112345', 'Receipt', 'RCT-0001', 'Office Supplies Receipt', 'Purchased stationery and printer ink', '/docs/rct001.pdf', '2024-02-05', 'Active'),

                                    -- PersonId 1002
                                    (1002, '85050567890', 'Invoice', 'INV-0002', 'Product Purchase Invoice', 'Hardware equipment', '/docs/inv002.pdf', '2024-02-20', 'Active'),
                                    (1002, '85050567890', 'Report', 'RPT-0001', 'Annual Financial Report', 'Year 2023 summary', '/docs/rpt001.pdf', '2024-03-01', 'Active'),
                                    (1002, '85050567890', 'Agreement', 'AGR-0001', 'NDA with Vendor', 'Non-disclosure agreement for project X', '/docs/agr001.pdf', '2024-03-10', 'Active'),
                                    (1002, '85050567890', 'License', 'LIC-0001', 'Software License', 'Annual subscription for DesignPro', '/docs/lic001.pdf', '2024-04-15', 'Active'),

                                    -- PersonId 1003
                                    (1003, '92120398765', 'Certificate', 'CERT-0001', 'Training Certificate', 'Completed professional training', '/docs/cert001.pdf', '2024-03-15', 'Active'),
                                    (1003, '92120398765', 'Application', 'APP-0001', 'Loan Application', 'Personal loan request', '/docs/app001.pdf', '2024-04-01', 'Pending'),
                                    (1003, '92120398765', 'Warranty', 'WRR-0001', 'Product Warranty', '2-year warranty for laptop', '/docs/wrr001.pdf', '2024-05-20', 'Active'),

                                    -- PersonId 1004
                                    (1004, '88071545678', 'Invoice', 'INV-0003', 'Consulting Invoice', 'IT consulting services', '/docs/inv003.pdf', '2024-05-10', 'Active'),
                                    (1004, '88071545678', 'Proposal', 'PRO-0001', 'Project Proposal', 'Proposal for system upgrade', '/docs/pro001.pdf', '2024-06-05', 'Draft'),
                                    (1004, '88071545678', 'Certificate', 'CERT-0002', 'Safety Training', 'Workplace safety certification', '/docs/cert002.pdf', '2024-06-15', 'Active'),
                                    (1004, '88071545678', 'Receipt', 'RCT-0002', 'Travel Expenses', 'Hotel and transport receipts', '/docs/rct002.pdf', '2024-07-01', 'Active'),

                                    -- PersonId 1005
                                    (1005, '95032012345', 'Contract', 'CNT-0002', 'Freelance Contract', '6-month freelance agreement', '/docs/cnt002.pdf', '2024-07-10', 'Active'),
                                    (1005, '95032012345', 'Invoice', 'INV-0004', 'Freelance Invoice', 'Monthly services invoice', '/docs/inv004.pdf', '2024-08-01', 'Active'),
                                    (1005, '95032012345', 'Report', 'RPT-0002', 'Quarterly Report', 'Q2 financial performance', '/docs/rpt002.pdf', '2024-08-15', 'Active'),
                                    (1005, '95032012345', 'Agreement', 'AGR-0002', 'Partnership Agreement', 'Joint venture with Company Y', '/docs/agr002.pdf', '2024-09-01', 'Active'),

                                    -- PersonId 1006
                                    (1006, '83112578901', 'Certificate', 'CERT-0003', 'First Aid Certificate', 'First aid training completion', '/docs/cert003.pdf', '2024-09-10', 'Active'),
                                    (1006, '83112578901', 'Invoice', 'INV-0005', 'Maintenance Invoice', 'Office maintenance services', '/docs/inv005.pdf', '2024-10-05', 'Active'),
                                    (1006, '83112578901', 'Receipt', 'RCT-0003', 'Office Lunch', 'Team lunch receipt', '/docs/rct003.pdf', '2024-10-15', 'Active'),
                                    (1006, '83112578901', 'License', 'LIC-0002', 'Cloud Storage License', 'Enterprise cloud storage plan', '/docs/lic002.pdf', '2024-11-01', 'Active'),

                                    -- PersonId 1007
                                    (1007, '91041834567', 'Application', 'APP-0002', 'Grant Application', 'Research grant proposal', '/docs/app002.pdf', '2024-11-10', 'Pending'),
                                    (1007, '91041834567', 'Report', 'RPT-0003', 'Research Report', 'Annual research findings', '/docs/rpt003.pdf', '2024-12-01', 'Active'),
                                    (1007, '91041834567', 'Warranty', 'WRR-0002', 'Equipment Warranty', '3-year warranty for lab equipment', '/docs/wrr002.pdf', '2024-12-15', 'Active'),

                                    -- PersonId 1008
                                    (1008, '86093078912', 'Proposal', 'PRO-0002', 'Marketing Proposal', 'Digital marketing campaign plan', '/docs/pro002.pdf', '2025-01-10', 'Draft'),
                                    (1008, '86093078912', 'Invoice', 'INV-0006', 'Advertising Invoice', 'Online ad campaign invoice', '/docs/inv006.pdf', '2025-01-20', 'Active'),
                                    (1008, '86093078912', 'Certificate', 'CERT-0004', 'Marketing Certification', 'Advanced digital marketing course', '/docs/cert004.pdf', '2025-02-05', 'Active'),

                                    -- PersonId 1009
                                    (1009, '93062245678', 'Contract', 'CNT-0003', 'Service Contract', 'Annual IT support contract', '/docs/cnt003.pdf', '2025-02-15', 'Active'),
                                    (1009, '93062245678', 'Agreement', 'AGR-0003', 'Service Level Agreement', 'SLA for IT services', '/docs/agr003.pdf', '2025-03-01', 'Active'),
                                    (1009, '93062245678', 'Receipt', 'RCT-0004', 'Software Purchase', 'New software licenses', '/docs/rct004.pdf', '2025-03-10', 'Active'),

                                    -- PersonId 1010
                                    (1010, '89081412345', 'Invoice', 'INV-0007', 'Consulting Invoice', 'Business consulting services', '/docs/inv007.pdf', '2025-04-01', 'Active'),
                                    (1010, '89081412345', 'Report', 'RPT-0004', 'Audit Report', 'Annual financial audit', '/docs/rpt004.pdf', '2025-04-15', 'Active'),
                                    (1010, '89081412345', 'License', 'LIC-0003', 'Security Software License', 'Enterprise security suite', '/docs/lic003.pdf', '2025-05-01', 'Active'),

                                    -- PersonId 1011
                                    (1011, '87021965432', 'Diploma', 'DIP-0001', 'University Diploma', 'Bachelor of Science in Computer Science', '/docs/dip001.pdf', '2025-05-15', 'Active'),
                                    (1011, '87021965432', 'Resume', 'RES-0001', 'Professional Resume', 'Updated resume for job applications', '/docs/res001.pdf', '2025-05-20', 'Active'),
                                    (1011, '87021965432', 'Reference', 'REF-0001', 'Employment Reference', 'Reference letter from previous employer', '/docs/ref001.pdf', '2025-05-25', 'Active'),

                                    -- PersonId 1012
                                    (1012, '94091178901', 'Insurance', 'INS-0001', 'Health Insurance Policy', 'Annual health insurance coverage', '/docs/ins001.pdf', '2025-06-01', 'Active'),
                                    (1012, '94091178901', 'Claim', 'CLM-0001', 'Insurance Claim', 'Claim for medical expenses', '/docs/clm001.pdf', '2025-06-10', 'Pending'),
                                    (1012, '94091178901', 'Policy', 'POL-0001', 'Privacy Policy', 'Company privacy policy document', '/docs/pol001.pdf', '2025-06-15', 'Active'),

                                    -- PersonId 1013
                                    (1013, '82102432165', 'Manual', 'MAN-0001', 'User Manual', 'Software user guide', '/docs/man001.pdf', '2025-07-01', 'Active'),
                                    (1013, '82102432165', 'Guide', 'GDE-0001', 'Installation Guide', 'Step-by-step installation instructions', '/docs/gde001.pdf', '2025-07-10', 'Active'),
                                    (1013, '82102432165', 'Form', 'FRM-0001', 'Feedback Form', 'Customer feedback collection form', '/docs/frm001.pdf', '2025-07-15', 'Active'),

                                    -- PersonId 1014
                                    (1014, '96011745678', 'Transcript', 'TRN-0001', 'Academic Transcript', 'Official university transcript', '/docs/trn001.pdf', '2025-08-01', 'Active'),
                                    (1014, '96011745678', 'Letter', 'LTR-0001', 'Recommendation Letter', 'Recommendation for graduate school', '/docs/ltr001.pdf', '2025-08-10', 'Active'),
                                    (1014, '96011745678', 'Statement', 'STM-0001', 'Bank Statement', 'Monthly bank statement', '/docs/stm001.pdf', '2025-08-15', 'Active'),

                                    -- PersonId 1015
                                    (1015, '84053098765', 'Permit', 'PRM-0001', 'Work Permit', 'Temporary work permit document', '/docs/prm001.pdf', '2025-09-01', 'Active'),
                                    (1015, '84053098765', 'Ticket', 'TKT-0001', 'Support Ticket', 'IT support request and resolution', '/docs/tkt001.pdf', '2025-09-10', 'Closed'),
                                    (1015, '84053098765', 'Survey', 'SRV-0001', 'Customer Survey', 'Annual customer satisfaction survey', '/docs/srv001.pdf', '2025-09-15', 'Active'),

                                    -- PersonId 1016
                                    (1016, '90120532165', 'Memo', 'MEM-0001', 'Internal Memo', 'Company-wide policy update memo', '/docs/mem001.pdf', '2025-10-01', 'Active'),
                                    (1016, '90120532165', 'Minutes', 'MIN-0001', 'Meeting Minutes', 'Minutes from board meeting', '/docs/min001.pdf', '2025-10-10', 'Active'),
                                    (1016, '90120532165', 'Newsletter', 'NWS-0001', 'Company Newsletter', 'Monthly internal newsletter', '/docs/nws001.pdf', '2025-10-15', 'Active'),

                                    -- PersonId 1017
                                    (1017, '81031865432', 'Budget', 'BGT-0001', 'Project Budget', 'Budget proposal for new project', '/docs/bgt001.pdf', '2025-11-01', 'Draft'),
                                    (1017, '81031865432', 'Schedule', 'SCH-0001', 'Project Schedule', 'Timeline for project delivery', '/docs/sch001.pdf', '2025-11-10', 'Active'),
                                    (1017, '81031865432', 'Checklist', 'CHK-0001', 'Onboarding Checklist', 'New employee onboarding tasks', '/docs/chk001.pdf', '2025-11-15', 'Active'),

                                    -- PersonId 1018
                                    (1018, '97072298765', 'Presentation', 'PRE-0001', 'Sales Presentation', 'Quarterly sales performance presentation', '/docs/pre001.pdf', '2025-12-01', 'Active'),
                                    (1018, '97072298765', 'Brochure', 'BRH-0001', 'Product Brochure', 'Marketing brochure for new product', '/docs/brh001.pdf', '2025-12-10', 'Active'),
                                    (1018, '97072298765', 'Catalog', 'CTG-0001', 'Product Catalog', 'Updated product catalog for 2026', '/docs/ctg001.pdf', '2025-12-15', 'Active'),

                                    -- PersonId 1019
                                    (1019, '05280129353', 'Report', 'RPT-0005', 'Financial Report Q1', 'Detailed financial report for Q1 2025', '/docs/rpt005.pdf', '2025-01-15', 'Active'),
                                    (1019, '05280129353', 'Report', 'RPT-0006', 'Financial Report Q2', 'Detailed financial report for Q2 2025', '/docs/rpt006.pdf', '2025-04-15', 'Active'),
                                    (1019, '05280129353', 'Report', 'RPT-0007', 'Financial Report Q3', 'Detailed financial report for Q3 2025', '/docs/rpt007.pdf', '2025-07-15', 'Active'),
                                    (1019, '05280129353', 'Report', 'RPT-0008', 'Financial Report Q4', 'Detailed financial report for Q4 2025', '/docs/rpt008.pdf', '2025-10-15', 'Active'),

                                    -- PersonId 1020
                                    (1020, '08261437865', 'Invoice', 'INV-0008', 'Invoice January', 'Invoice for January services', '/docs/inv008.pdf', '2025-01-31', 'Active'),
                                    (1020, '08261437865', 'Invoice', 'INV-0009', 'Invoice February', 'Invoice for February services', '/docs/inv009.pdf', '2025-02-28', 'Active'),
                                    (1020, '08261437865', 'Invoice', 'INV-0010', 'Invoice March', 'Invoice for March services', '/docs/inv010.pdf', '2025-03-31', 'Active'),
                                    (1020, '08261437865', 'Invoice', 'INV-0011', 'Invoice April', 'Invoice for April services', '/docs/inv011.pdf', '2025-04-30', 'Active'),
                                    (1020, '08261437865', 'Invoice', 'INV-0012', 'Invoice May', 'Invoice for May services', '/docs/inv012.pdf', '2025-05-31', 'Active'),
                                    (1020, '08261437865', 'Invoice', 'INV-0013', 'Invoice June', 'Invoice for June services', '/docs/inv013.pdf', '2025-06-30', 'Active'),

                                    -- PersonId 1021
                                    (1021, '99010758267', 'Memo', 'MEM-0002', 'HR Memo', 'HR policy update memo', '/docs/mem002.pdf', '2025-01-10', 'Active'),
                                    (1021, '99010758267', 'Memo', 'MEM-0003', 'Office Memo', 'Office closure announcement', '/docs/mem003.pdf', '2025-05-10', 'Active'),
                                    (1021, '99010758267', 'Memo', 'MEM-0004', 'Safety Memo', 'Workplace safety updates', '/docs/mem004.pdf', '2025-09-10', 'Active'),

                                    -- PersonId 1022
                                    (1022, '66112435932', 'Contract', 'CTR-0001', 'Client Contract A', 'Contract agreement with Client A', '/docs/ctr001.pdf', '2025-02-01', 'Active'),
                                    (1022, '66112435932', 'Contract', 'CTR-0002', 'Client Contract B', 'Contract agreement with Client B', '/docs/ctr002.pdf', '2025-03-01', 'Active'),
                                    (1022, '66112435932', 'Contract', 'CTR-0003', 'Vendor Contract', 'Contract agreement with Vendor X', '/docs/ctr003.pdf', '2025-06-01', 'Active'),

                                    -- PersonId 1023
                                    (1023, '84101192381', 'Policy', 'PLC-0001', 'Privacy Policy', 'Updated company privacy policy', '/docs/plc001.pdf', '2025-01-20', 'Active'),
                                    (1023, '84101192381', 'Policy', 'PLC-0002', 'Security Policy', 'Updated IT security policy', '/docs/plc002.pdf', '2025-04-20', 'Active'),
                                    (1023, '84101192381', 'Policy', 'PLC-0003', 'Leave Policy', 'Employee leave policy update', '/docs/plc003.pdf', '2025-08-20', 'Active'),

                                    -- PersonId 1024
                                    (1024, '70032189288', 'Form', 'FRM-0002', 'Leave Application Form', 'Form for applying employee leave', '/docs/frm002.pdf', '2025-01-05', 'Active'),
                                    (1024, '70032189288', 'Form', 'FRM-0003', 'Expense Form', 'Form for submitting expenses', '/docs/frm003.pdf', '2025-03-05', 'Active'),
                                    (1024, '70032189288', 'Form', 'FRM-0004', 'Feedback Form', 'Customer feedback submission form', '/docs/frm004.pdf', '2025-06-05', 'Active'),

                                    -- PersonId 1025
                                    (1025, '77092655919', 'Letter', 'LTR-0002', 'Offer Letter', 'Job offer letter for new hire', '/docs/ltr002.pdf', '2025-02-15', 'Active'),
                                    (1025, '77092655919', 'Letter', 'LTR-0003', 'Resignation Letter', 'Formal resignation letter', '/docs/ltr003.pdf', '2025-05-15', 'Active'),
                                    (1025, '77092655919', 'Letter', 'LTR-0004', 'Recommendation Letter', 'Letter of recommendation', '/docs/ltr004.pdf', '2025-09-15', 'Active'),

                                    -- PersonId 1026
                                    (1026, '76072625775', 'Manual', 'MNL-0001', 'User Manual', 'User manual for product A', '/docs/mnl001.pdf', '2025-02-28', 'Active'),
                                    (1026, '76072625775', 'Manual', 'MNL-0002', 'Installation Guide', 'Installation manual for product B', '/docs/mnl002.pdf', '2025-06-28', 'Active'),
                                    (1026, '76072625775', 'Manual', 'MNL-0003', 'Training Manual', 'Internal training documentation', '/docs/mnl003.pdf', '2025-10-28', 'Active');
                            ";
                                await command.ExecuteNonQueryAsync();

                                // Insert sample user history
                                command.CommandText = @"
                                    INSERT INTO UserHistoryUsingCompanyDocuments (UserId, UserQuery, Action, DocumentId, CreatedAt)
                                    VALUES 
                                        -- User 1 (john.doe) activity
                                        (1, '90010112345', 'acceptedSearch', 1, '2024-01-16 10:30:00'),
                                        (1, '90010112345', 'Click', 1, '2024-01-16 10:31:00'),
                                        (1, 'INV-2024-001', 'Read', 1, '2024-01-16 10:32:00'),
                                        (1, '90010112345', 'Click', 2, '2024-01-16 10:35:00'),
                                        (1, 'Contract', 'Download', 2, '2024-01-16 10:36:00'),
                                        
                                        -- User 2 (jane.smith) activity
                                        (2, '85050567890', 'acceptedSearch', 3, '2024-02-21 14:15:00'),
                                        (2, 'INV-2024-002', 'Click', 3, '2024-02-21 14:16:00'),
                                        (2, 'Product Purchase Invoice', 'Read', 3, '2024-02-21 14:17:00'),
                                        (2, '85050567890', 'acceptedSearch', 4, '2024-03-02 09:00:00'),
                                        (2, 'RPT-2024-001', 'use RAG', 4, '2024-03-02 09:05:00'),
                                        
                                        -- User 3 (admin.user) activity
                                        (3, '92120398765', 'acceptedSearch', 5, '2024-03-16 11:20:00'),
                                        (3, 'CERT-2024-001', 'Click', 5, '2024-03-16 11:21:00'),
                                        (3, 'Training Certificate', 'Download', 5, '2024-03-16 11:22:00'),
                                        (3, '88071545678', 'acceptedSearch', 6, '2024-05-11 08:45:00'),
                                        (3, 'INV-2024-003', 'Read', 6, '2024-05-11 08:46:00'),
                                        
                                        -- User 4 (bob.johnson) activity
                                        (4, '95032012345', 'acceptedSearch', 10, '2024-07-11 13:30:00'),
                                        (4, 'CNT-2024-002', 'Click', 10, '2024-07-11 13:31:00'),
                                        (4, 'Freelance Contract', 'use RAG', 10, '2024-07-11 13:35:00'),
                                        (4, '95032012345', 'acceptedSearch', 11, '2024-08-02 10:00:00'),
                                        (4, 'INV-2024-004', 'Download', 11, '2024-08-02 10:05:00'),
                                        
                                        -- User 5 (alice.williams) activity
                                        (5, '83112578901', 'acceptedSearch', 14, '2024-09-11 15:20:00'),
                                        (5, 'CERT-2024-003', 'Click', 14, '2024-09-11 15:21:00'),
                                        (5, 'First Aid Certificate', 'Read', 14, '2024-09-11 15:22:00'),
                                        (5, '05280129353', 'acceptedSearch', 58, '2025-01-16 09:00:00'),
                                        (5, 'RPT-2025-001', 'use RAG', 58, '2025-01-16 09:10:00'),
                                        
                                        -- Additional mixed activity
                                        (1, '08261437865', 'acceptedSearch', 62, '2025-02-01 11:00:00'),
                                        (1, 'INV-2025-001', 'Click', 62, '2025-02-01 11:01:00'),
                                        (2, '99010758267', 'acceptedSearch', 68, '2025-01-11 12:00:00'),
                                        (2, 'MEM-2025-001', 'Read', 68, '2025-01-11 12:05:00'),
                                        (3, '66112435932', 'acceptedSearch', 71, '2025-02-02 14:30:00'),
                                        (3, 'CTR-2025-001', 'Download', 71, '2025-02-02 14:35:00');
                                ";
                                await command.ExecuteNonQueryAsync();
                            }
                        }

                    
                

UserHistoryRepository

                    
                            // Initialize database using the properly built service provider
                            var dbConfig = app.Services.GetRequiredService();
                            await DatabaseInitializer.InitializeDatabaseAsync(dbConfig.ConnectionString);

                    
                

MCPServerDataExportTools o Historię użytkowników

                    



























































                        using MCPServerDocuments.Database.Document;
                        using MCPServerDocuments.DataClasses;
                        using MCPServerDocuments.Tools;
                        using ModelContextProtocol.Server;
                        using MyMPCServer.Database;
                        using System.ComponentModel;
                        using System.Text.Json;

                        namespace MCPServerDocuments;

                        [McpServerToolType]
                        public sealed class MCPServerDataExportTools
                        {
                            private readonly CompanyDocumentsRepository _repository;
                            private readonly UserHistoryRepository _historyRepository;

                            public MCPServerDataExportTools(
                                CompanyDocumentsRepository repository,
                                UserHistoryRepository historyRepository)
                            {
                                _repository = repository;
                                _historyRepository = historyRepository;
                            }

                        …………….

                            [McpServerTool, Description("Get all user history with full details including user names and document information.")]
                            public async Task<string> GetAllUserHistoryWithDetails()
                            {
                                try
                                {
                                    var historyDetails = await _historyRepository.GetAllHistoryWithDetailsAsync();

                                    return JsonSerializer.Serialize(new GetAllUserHistoryResult
                                    {
                                        Success = true,
                                        Message = $"Found {historyDetails.Count} history record(s)",
                                        RecordCount = historyDetails.Count,
                                        HistoryRecords = historyDetails,
                                        Timestamp = DateTime.UtcNow
                                    }, UserHistoryJsonContext.Default.GetAllUserHistoryResult);
                                }
                                catch (Exception ex)
                                {
                                    return JsonSerializer.Serialize(new UserHistoryError
                                    {
                                        Success = false,
                                        Error = ex.Message,
                                        Timestamp = DateTime.UtcNow
                                    }, UserHistoryJsonContext.Default.UserHistoryError);
                                }
                            }


                        }

                        public partial class GetAllUserHistoryResult
                        {
                            public bool Success { get; set; }
                            public string Message { get; set; } = string.Empty;
                            public int RecordCount { get; set; }
                            public List<UserHistoryWithDetails> HistoryRecords { get; set; } = new();
                            public DateTime Timestamp { get; set; }
                        }

                        public partial class UserHistoryError
                        {
                            public bool Success { get; set; }
                            public string Error { get; set; } = string.Empty;
                            public DateTime Timestamp { get; set; }
                        }


                    
                
blog
CW
Jak duża może być taka
historia użytkowników?
  1. Historia użytkowników
Czyli skracamy odpowiedzi
z MCP jak się tylko da
  1. Historia użytkowników
Jest to też dobry przykład problemu nazewnictwa i opisu "MCP metod"
Czasem głupszy lokalny LLM miał problem z wyborem narzędzia
bo pomylił "osoby id"
z "historią użytkownika"
Co znaczy, że wywołał złą metodę
Czyli dla serwerów MCP trzeba zasotować DDD i Bounded Contexty?
Jak wygląda wysłka
Email i Excel?
blog
CW
Mailgun usługa do wysyłania emaili
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
Pokaż kod ExcelService
i MailgunEmailService

ExcelService

                    



































































                        using MCPServerDocuments.DataClasses;
                        using OfficeOpenXml;
                        using OfficeOpenXml.Style;
                        using System.Drawing;

                        namespace MCPServerDocuments.Tools;

                        public class ExcelService
                        {
                            public async Task<byte[]> SaveDocumentsAsExcelAsync(List<CompanyDocument> documents, string fileNamePrefix)
                            {
                                try
                                {
                                    ExcelPackage.LicenseContext = LicenseContext.NonCommercial;

                                    using var package = new ExcelPackage();
                                    var worksheet = package.Workbook.Worksheets.Add("Document Export");

                                    // Headers
                                    worksheet.Cells[1, 1].Value = "ID";
                                    worksheet.Cells[1, 2].Value = "Person ID";
                                    worksheet.Cells[1, 3].Value = "PESEL";
                                    worksheet.Cells[1, 4].Value = "Document Type";
                                    worksheet.Cells[1, 5].Value = "Document Number";
                                    worksheet.Cells[1, 6].Value = "Title";
                                    worksheet.Cells[1, 7].Value = "Description";
                                    worksheet.Cells[1, 8].Value = "File Path";
                                    worksheet.Cells[1, 9].Value = "Created At";
                                    worksheet.Cells[1, 10].Value = "Modified At";
                                    worksheet.Cells[1, 11].Value = "Status";

                                    // Data rows
                                    for (int i = 0; i < documents.Count; i++)
                                    {
                                        var row = i + 2;
                                        worksheet.Cells[row, 1].Value = documents[i].Id;
                                        worksheet.Cells[row, 2].Value = documents[i].PersonId;
                                        worksheet.Cells[row, 3].Value = documents[i].Pesel;
                                        worksheet.Cells[row, 4].Value = documents[i].DocumentType;
                                        worksheet.Cells[row, 5].Value = documents[i].DocumentNumber;
                                        worksheet.Cells[row, 6].Value = documents[i].Title;
                                        worksheet.Cells[row, 7].Value = documents[i].Description;
                                        worksheet.Cells[row, 8].Value = documents[i].FilePath;
                                        worksheet.Cells[row, 9].Value = documents[i].CreatedAt.ToString("yyyy-MM-dd HH:mm:ss");
                                        worksheet.Cells[row, 10].Value = documents[i].ModifiedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
                                        worksheet.Cells[row, 11].Value = documents[i].Status;
                                    }

                                    FormatWorksheet(worksheet, 11, documents.Count);

                                    // Save to memory
                                    await using var stream = new MemoryStream();
                                    await package.SaveAsAsync(stream);
                                    return stream.ToArray();
                                }
                                catch (Exception ex)
                                {
                                    throw new Exception($"Failed to create Excel file in memory: {ex.Message}", ex);
                                }
                            }

                            private void FormatWorksheet(ExcelWorksheet worksheet, int columnCount, int rowCount)
                            {
                                // Format header row
                                using (var range = worksheet.Cells[1, 1, 1, columnCount])
                                {
                                    range.Style.Font.Bold = true;
                                    range.Style.Fill.PatternType = ExcelFillStyle.Solid;
                                    range.Style.Fill.BackgroundColor.SetColor(Color.LightGray);
                                    range.Style.Border.BorderAround(ExcelBorderStyle.Thin);
                                }

                                // Auto-fit columns
                                for (int col = 1; col <= columnCount; col++)
                                {
                                    worksheet.Column(col).AutoFit();
                                }

                                // Format data rows with borders
                                if (rowCount > 0)
                                {
                                    using (var range = worksheet.Cells[2, 1, rowCount + 1, columnCount])
                                    {
                                        range.Style.Border.BorderAround(ExcelBorderStyle.Thin);
                                        range.Style.Border.Bottom.Style = ExcelBorderStyle.Thin;
                                    }
                                }
                            }
                        }

                    
                

MailgunEmailService

                    














































































































































                        using Microsoft.Extensions.Logging;
                        using System.Net.Http.Headers;
                        using System.Text;

                        namespace MCPServerDocuments.Tools;

                        public class MailgunEmailService
                        {
                            private readonly MailgunConfig config;
                            private readonly ILogger<MailgunEmailService> logger;
                            private readonly HttpClient httpClient;

                            public MailgunEmailService(MailgunConfig config, ILogger<MailgunEmailService> logger)
                            {
                                this.config = config ?? throw new ArgumentNullException(nameof(config));
                                this.logger = logger ?? throw new ArgumentNullException(nameof(logger));

                                if (string.IsNullOrWhiteSpace(config.ApiKey))
                                    throw new InvalidOperationException("Mailgun API key is not configured.");
                                if (string.IsNullOrWhiteSpace(config.Domain))
                                    throw new InvalidOperationException("Mailgun domain is not configured.");

                                httpClient = new HttpClient();
                                var authToken = Convert.ToBase64String(Encoding.ASCII.GetBytes($"api:{config.ApiKey}"));
                                httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authToken);
                            }

                            /// <summary>
                            /// Sends an email with an Excel file attachment via Mailgun.
                            /// </summary>
                            /// <param name="filePath">Path to the Excel file to attach</param>
                            /// <param name="recipient">Recipient email (optional; uses default if null)</param>
                            /// <param name="subject">Email subject (optional)</param>
                            /// <param name="messageBody">Email body text (optional)</param>
                            /// <returns>Mailgun Message-ID if successful</returns>
                            public async Task<string> SendExcelEmailAsync(
                                string filePath,
                                string? recipient = null,
                                string? subject = null,
                                string? messageBody = null)
                            {
                                if (string.IsNullOrWhiteSpace(filePath))
                                    throw new ArgumentException("File path cannot be null or empty.", nameof(filePath));

                                if (!File.Exists(filePath))
                                    throw new FileNotFoundException("Excel file not found.", filePath);

                                var toEmail = !string.IsNullOrWhiteSpace(recipient)
                                    ? recipient
                                    : config.DefaultRecipient;

                                if (string.IsNullOrWhiteSpace(toEmail))
                                    throw new InvalidOperationException("No recipient email address provided and no default configured.");

                                var emailSubject = subject ?? "Document Export Attached";
                                var emailContent = messageBody ?? "Please find the requested document export attached.";

                                using var form = new MultipartFormDataContent
                                {
                                    { new StringContent($"{config.FromName} <{config.FromEmail}>"), "from" },
                                    { new StringContent(toEmail), "to" },
                                    { new StringContent(emailSubject), "subject" },
                                    { new StringContent(emailContent), "text" }
                                };

                                // Add Excel attachment
                                var fileBytes = await File.ReadAllBytesAsync(filePath);
                                var fileName = Path.GetFileName(filePath);
                                var fileContent = new ByteArrayContent(fileBytes);
                                fileContent.Headers.ContentType = new MediaTypeHeaderValue(
                                    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
                                form.Add(fileContent, "attachment", fileName);

                                var url = $"https://api.mailgun.net/v3/{config.Domain}/messages";
                                var response = await httpClient.PostAsync(url, form);
                                var responseBody = await response.Content.ReadAsStringAsync();

                                if (!response.IsSuccessStatusCode)
                                {
                                    logger.LogError("Failed to send email via Mailgun. Status: {StatusCode}, Error: {Error}",
                                        response.StatusCode, responseBody);

                                    Console.WriteLine("Failed to send email via Mailgun.Status: { StatusCode}, Error: { Error}",
                                        response.StatusCode, responseBody);

                                    throw new InvalidOperationException($"Mailgun email failed: {responseBody}");
                                }

                                // Mailgun responds with JSON like: { "id": "<20231002123456.12345.67890@sandbox.mailgun.org>", "message": "Queued. Thank you." }
                                var messageId = ExtractMessageId(responseBody) ?? "unknown";

                                logger.LogInformation("Email sent successfully via Mailgun. Message-ID: {MessageId}, To: {To}",
                                    messageId, toEmail);

                                return messageId;
                            }

                            public async Task<string> SendExcelEmailAsync(
                            byte[] fileBytes,
                            string fileName,
                            string? recipient = null,
                            string? subject = null,
                            string? messageBody = null)
                            {
                                var toEmail = !string.IsNullOrWhiteSpace(recipient)
                                    ? recipient
                                    : config.DefaultRecipient;

                                if (string.IsNullOrWhiteSpace(toEmail))
                                    throw new InvalidOperationException("No recipient email address provided and no default configured.");

                                var emailSubject = subject ?? "Document Export Attached";
                                var emailContent = messageBody ?? "Please find the requested document export attached.";

                                using var form = new MultipartFormDataContent
                            {
                                { new StringContent($"{config.FromName} <{config.FromEmail}>"), "from" },
                                { new StringContent(toEmail), "to" },
                                { new StringContent(emailSubject), "subject" },
                                { new StringContent(emailContent), "text" }
                            };

                                // Attach in-memory Excel file
                                var fileContent = new ByteArrayContent(fileBytes);
                                fileContent.Headers.ContentType = new MediaTypeHeaderValue(
                                    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
                                form.Add(fileContent, "attachment", fileName);

                                var url = $"https://api.mailgun.net/v3/{config.Domain}/messages";
                                var response = await httpClient.PostAsync(url, form);
                                var responseBody = await response.Content.ReadAsStringAsync();

                                if (!response.IsSuccessStatusCode)
                                {
                                    logger.LogError("Failed to send email via Mailgun. Status: {StatusCode}, Error: {Error}",
                                        response.StatusCode, responseBody);
                                    throw new InvalidOperationException($"Mailgun email failed: {responseBody}");
                                }

                                return ExtractMessageId(responseBody) ?? "unknown";
                            }


                            private static string? ExtractMessageId(string responseBody)
                            {
                                // Very simple extraction (could use JSON parser like System.Text.Json if desired)
                                var marker = "\"id\":";
                                var idx = responseBody.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
                                if (idx >= 0)
                                {
                                    var start = responseBody.IndexOf('<', idx);
                                    var end = responseBody.IndexOf('>', start);
                                    if (start >= 0 && end > start)
                                    {
                                        return responseBody.Substring(start, end - start + 1);
                                    }
                                }
                                return null;
                            }
                        }

                        public class MailgunConfig
                        {
                            public string ApiKey { get; set; } = string.Empty;
                            public string Domain { get; set; } = string.Empty; // e.g. sandbox12345.mailgun.org
                            public string FromEmail { get; set; } = string.Empty;
                            public string FromName { get; set; } = string.Empty;
                            public string DefaultRecipient { get; set; } = string.Empty;
                        }


                    
                

MCPServerDataExportTools z email

                    



















































































                   









                        using MCPServerDocuments.Database.Document;
                        using MCPServerDocuments.DataClasses;
                        using MCPServerDocuments.Tools;
                        using ModelContextProtocol.Server;
                        using MyMPCServer.Database;
                        using System.ComponentModel;
                        using System.Text.Json;

                        namespace MCPServerDocuments;

                        [McpServerToolType]
                        public sealed class MCPServerDataExportTools
                        {
                            private readonly CompanyDocumentsRepository _repository;
                            private readonly UserHistoryRepository _historyRepository;

                            private readonly ExcelService _excelService;
                            private readonly MailgunEmailService _mailgunEmailService;

                            public MCPServerDataExportTools(
                                CompanyDocumentsRepository repository,
                                UserHistoryRepository historyRepository
                        ,
                                ExcelService excelService,
                                MailgunEmailService mailgunEmailService)
                            {
                                _repository = repository;
                                _historyRepository = historyRepository;
                                _excelService = excelService;
                                _mailgunEmailService = mailgunEmailService;
                            }

                            [McpServerTool, Description("Search by Person ID, export to Excel in-memory, and send via email to recipient")]
                            public async Task<string> ProcessDocumentWorkflowByPersonId(
                                [Description("Person ID to search for")] int personId,
                                [Description("Email recipient (optional)")] string? recipient = null)
                            {
                                try
                                {
                                    var documents = await _repository.GetDocumentsByPersonIdAsync(personId);
                                    if (documents.Count == 0)
                                    {
                                        return JsonSerializer.Serialize(new
                                        {
                                            success = false,
                                            error = $"No documents found for Person ID: {personId}",
                                            timestamp = DateTime.UtcNow
                                        }, ExcelAndEmailJsonContext.Default.ExcelError);
                                    }

                                    // Generate Excel in memory
                                    var fileBytes = await _excelService.SaveDocumentsAsExcelAsync(documents, $"PersonID_{personId}");
                                    var fileName = $"PersonID_{personId}_export.xlsx";

                                    // Send with Mailgun
                                    var mailgunMessageId = await _mailgunEmailService.SendExcelEmailAsync(
                                        fileBytes,
                                        fileName,
                                        recipient,
                                        $"Document Export for Person ID: {personId}",
                                        $"Please find attached {documents.Count} document(s) for Person ID {personId}.");

                                    return JsonSerializer.Serialize(new ExcelSuccess
                                    {
                                        Message = "Complete workflow executed successfully",
                                        PersonId = personId,
                                        DocumentCount = documents.Count,
                                        FileName = fileName,
                                        FilePath = $"(in-memory only)",
                                        EmailSentTo = recipient ?? "default recipient",
                                        EmailService = "Mailgun",
                                        MailgunMessageId = mailgunMessageId
                                    }, ExcelAndEmailJsonContext.Default.ExcelSuccess);
                                }
                                catch (Exception ex)
                                {
                                    return JsonSerializer.Serialize(new
                                    {
                                        success = false,
                                        error = ex.Message,
                                        timestamp = DateTime.UtcNow
                                    }, ExcelAndEmailJsonContext.Default.ExcelError);
                                }
                            }


                        }


                        public class ExcelError
                        {
                            public bool Success { get; set; } = false;

                            public string Error { get; set; } = string.Empty;

                            public DateTime Timestamp { get; set; } = DateTime.UtcNow;
                        }

                        public class ExcelSuccess
                        {
                            public bool Success { get; set; } = true;

                            public string Message { get; set; } = string.Empty;

                            public int PersonId { get; set; }

                            public int DocumentCount { get; set; }

                            public string FilePath { get; set; } = string.Empty;

                            public string FileName { get; set; } = string.Empty;

                            public string EmailSentTo { get; set; } = string.Empty;

                            public string EmailService { get; set; } = string.Empty;

                            public string? MailgunMessageId { get; set; }

                            public DateTime Timestamp { get; set; } = DateTime.UtcNow;
                        }


                    
                

Program.cs

                    


















































                    
                        


                        using MCPServerDocuments;
                        using MCPServerDocuments.Database;
                        using MCPServerDocuments.Database.Document;
                        using MCPServerDocuments.DataClasses;
                        using MCPServerDocuments.Tools;
                        using Microsoft.Extensions.DependencyInjection;
                        using Microsoft.Extensions.Hosting;
                        using Microsoft.Extensions.Logging;
                        using MyMPCServer.Database;
                        using System.Text.Json.Serialization;

                        var builder = Host.CreateEmptyApplicationBuilder(settings: null);
                        builder.Services
                            .AddMcpServer()
                            .WithStdioServerTransport()
                            .WithTools<MCPServerDataExportTools>();

                        builder.Logging.AddConsole();
                        builder.Logging.SetMinimumLevel(LogLevel.Debug);

                        // Register repositories
                        builder.Services.AddSingleton<CompanyDocumentsRepository>();
                        builder.Services.AddSingleton<UserHistoryRepository>();
                        builder.Services.AddSingleton<ExcelService>();

                        var defaultDbPath = Path.Combine(
                            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
                            "MCPServerDocuments",
                            "documents.db"
                        );

                        builder.Services.AddSingleton<DatabaseConfig>(provider => new DatabaseConfig
                        {
                            ConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING") ?? defaultDbPath
                        });

                        builder.Services.AddSingleton<MailgunConfig>(provider => new MailgunConfig
                        {
                            ApiKey = Environment.GetEnvironmentVariable("MAILGUN_API_KEY"),
                            Domain = Environment.GetEnvironmentVariable("MAILGUN_DOMAIN"),
                            FromEmail = Environment.GetEnvironmentVariable("MAILGUN_FROM_EMAIL"),
                            FromName = Environment.GetEnvironmentVariable("MAILGUN_FROM_NAME"),
                            DefaultRecipient = Environment.GetEnvironmentVariable("DEFAULT_RECIPIENT"),
                        });

                        // Register the email service
                        builder.Services.AddSingleton<MailgunEmailService>();

                        // Initialize database - use a separate scope or do this after Build()
                        var app = builder.Build();

                        // Initialize database using the properly built service provider
                        var dbConfig = app.Services.GetRequiredService<DatabaseConfig>();
                        await DatabaseInitializer.InitializeDatabaseAsync(dbConfig.ConnectionString);

                        await app.RunAsync();


                        // JSON serialization context
                        [JsonSerializable(typeof(SearchDocumentsByPeselResult))]
                        [JsonSerializable(typeof(SearchDocumentsError))]
                        [JsonSerializable(typeof(SearchDocumentsByPersonIdResult))]
                        public partial class CompanyDocumentJsonContext : JsonSerializerContext
                        {
                        }

                        [JsonSerializable(typeof(GetAllUserHistoryResult))]
                        [JsonSerializable(typeof(UserHistoryError))]
                        [JsonSerializable(typeof(List<UserHistoryWithDetails>))]
                        [JsonSerializable(typeof(UserHistoryWithDetails))]
                        public partial class UserHistoryJsonContext : JsonSerializerContext
                        {
                        }

                        [JsonSerializable(typeof(ExcelError))]
                        [JsonSerializable(typeof(ExcelSuccess))]
                        public partial class ExcelAndEmailJsonContext : JsonSerializerContext
                        {
                        }




                    
                
Czy to działa?
Aby MCPInspector mógł testować
to muszę mieć zmienne środowiskowe na komputerze
blog
CW

Wstrzykniecie tymczasowych zmiennych środowiskowych

                    

                        $env:MAILGUN_API_KEY = ""
                        $env:MAILGUN_DOMAIN = ".mailgun.org"
                        $env:MAILGUN_FROM_EMAIL = "postmaster@.mailgun.org"
                        $env:MAILGUN_FROM_NAME = "Data Export Service"
                        $env:DEFAULT_RECIPIENT = "@gmail.com"
                    
                
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
Czy to działa w edytorach?
Kontener dockera jakoś musi dostać zmienne środowiskowe
Pierwszy pomysł polegał na tym
aby mieć 1 słuszny obraz docker
ze wszystkim ukrytmi zmiennymi

LLM Studio json

                    

                        docker build -t mcp-server-documents:latest .

                        docker run  --name mcp-server-documents-container --rm -i --env-file docker-env-variables.env -v ${PWD}/data:/data -v ${PWD}/exports:/exports mcp-server-documents:latest

                        exec mcp-server-documents-container env
                    
                
blog
CW

W .continue wskazać na ten 1 słuszny obraz docker

                    

                        name: Local Agent
                        version: 1.0.0
                        schema: v1
                        models:
                        - name: Autodetect
                            provider: ollama
                            model: AUTODETECT
                        mcpServers:
                        - name: company-documents
                            command: docker
                            args:
                            - exec
                            - -i
                            - mcp-server-documents-container
                            - ./MCPServerDocuments
                            env: {}
                    
                
Drugi pomysł to przekazać zmienne środowiskowe
z edytorów do dockera
blog
CW

Wstrzykniecie tymczasowych zmiennych środowiskowych

                    




















                        









                        {
                        "mcpServers": {
                            "obsidian": {
                            "command": "docker",
                            "args": [
                                "run",
                                "-i",
                                "--rm",
                                "-e",
                                "OBSIDIAN_HOST",
                                "-e",
                                "OBSIDIAN_API_KEY",
                                "mcp/obsidian"
                            ],
                            "env": {
                                "OBSIDIAN_HOST": "host.docker.internal",
                                "OBSIDIAN_API_KEY": ""
                            }
                            },
                            "youtube_transcript": {
                            "command": "docker",
                            "args": [
                                "run",
                                "-i",
                                "--rm",
                                "mcp/youtube-transcript"
                            ]
                            },
                            "company-documents": {
                            "command": "docker",
                            "args": [
                                "run",
                                "-i",
                                "--rm",
                                "-v", "C:\\Users\\Cezary\\mcp-documents-data:/data",
                                "-e", "DB_CONNECTION_STRING=/data/documents.db",
                                "-e", "MAILGUN_API_KEY=",
                                "-e", "MAILGUN_DOMAIN=s",
                                "-e", "MAILGUN_FROM_EMAIL=",
                                "-e", "MAILGUN_FROM_NAME=Data Export Service",
                                "-e", "DEFAULT_RECIPIENT=crezber@gmail.com",
                                "mcp-server-documents:latest"
                            ],
                            "env": {
                                "DB_CONNECTION_STRING": "/data/documents.db",
                                "MAILGUN_API_KEY": ",
                                "MAILGUN_DOMAIN": ".mailgun.org",
                                "MAILGUN_FROM_EMAIL": "postmaster@.mailgun.org",
                                "MAILGUN_FROM_NAME" : "Data Export Service",
                                "DEFAULT_RECIPIENT": "@gmail.com"
                            }
                            }
                        }
                        }
                    
                

W .continue wskazać na ten 1 słuszny obraz docker

                    



                        # Use the official .NET SDK image for building
                        FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
                        WORKDIR /src

                        # Copy project files
                        COPY *.csproj ./
                        RUN dotnet restore

                        # Copy remaining source code
                        COPY . ./
                        # Publish as self-contained executable for linux-x64
                        RUN dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -o /app/publish

                        # Use minimal runtime image since we have self-contained exe
                        FROM mcr.microsoft.com/dotnet/runtime-deps:8.0
                        WORKDIR /app

                        # Copy published app
                        COPY --from=build /app/publish .

                        # Create directory for database
                        RUN mkdir -p /data
                        # TO DZIADOSTOW ZAKOMENTOWAĆ
                        # Set environment variable for database path
                        #ENV DB_CONNECTION_STRING=/data/documents.db

                        # Run the self-contained executable
                        ENTRYPOINT ["./MCPServerDocuments"]
                    
                
blog
CW
blog
CW
blog
CW
Co jeszcze warto dodać?
Jak działa MCP jako serwer HTTP?
Paczka NuGet do tworzenia serwerów MCP w C#
ulepsza się błyskawicznie
😎
blog
CW
blog
CW
blog
CW
  1. Co nam się nie podoba w tym podejściu lokalnym
blog
CW
blog
CW
  1. Czego się nauczyłem na tym etapie
  1. Złote zasady MCP
  1. O czym trzeba pamiętać przy MCP
  1. Jak nie mieć problemów z swoim MCP
blog
CW
blog
CW
  1. MCP, a Context Engineering
Czy wymyślili coś lepszego niż MCP?
🤔
blog
CW
  1. Problem MCP
  1. Alternatywa od MCP
Co jest lepsze niż MCP?
🤔
Pisanie kodu pod przypadek
xD
Widać też nacisk firm na agentów którzy mają "grzebać" nam w komputerze
  1. Agenci co mają "grzebać" w komputerze
Niech agent napisze skrypt i go uruchomi
blog
CW
blog
CW
blog
CW
blog
CW
Jeśli interesują was trendy w AI to polecam:
blog
CW
blog
CW
Wracamy do prezentacji o MCP
Co dziś jeszcze zbudujemy?
blog
CW
Czy wszystkie aplikacje wspierają protokół SSE/HTTP?
blog
CW
blog
CW
Uruchomiliśmy MCP przez stdio
blog
CW
Co wspiera protokół SSE/HTTP
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW

Wstrzykniecie tymczasowych zmiennych środowiskowych

                    




















                        













                        {
                        "mcpServers": {
                            "obsidian": {
                            "command": "docker",
                            "args": [
                                "run",
                                "-i",
                                "--rm",
                                "-e",
                                "OBSIDIAN_HOST",
                                "-e",
                                "OBSIDIAN_API_KEY",
                                "mcp/obsidian"
                            ],
                            "env": {
                                "OBSIDIAN_HOST": "host.docker.internal",
                                "OBSIDIAN_API_KEY": ""
                            }
                            },
                            "youtube_transcript": {
                            "command": "docker",
                            "args": [
                                "run",
                                "-i",
                                "--rm",
                                "mcp/youtube-transcript"
                            ]
                            },
                            "company-documents": {
                            "command": "docker",
                            "args": [
                                "run",
                                "-i",
                                "--rm",
                                "-v", "C:\\Users\\Cezary\\mcp-documents-data:/data",
                                "-e", "DB_CONNECTION_STRING=/data/documents.db",
                                "-e", "MAILGUN_API_KEY=",
                                "-e", "MAILGUN_DOMAIN=s",
                                "-e", "MAILGUN_FROM_EMAIL=",
                                "-e", "MAILGUN_FROM_NAME=Data Export Service",
                                "-e", "DEFAULT_RECIPIENT=crezber@gmail.com",
                                "mcp-server-documents:latest"
                            ],
                            "env": {
                                "DB_CONNECTION_STRING": "/data/documents.db",
                                "MAILGUN_API_KEY": ",
                                "MAILGUN_DOMAIN": ".mailgun.org",
                                "MAILGUN_FROM_EMAIL": "postmaster@.mailgun.org",
                                "MAILGUN_FROM_NAME" : "Data Export Service",
                                "DEFAULT_RECIPIENT": "@gmail.com"
                            }
                            },
                            ,
                            "company-documents-server": {
                                "url": "http://localhost:3001/mcp/sse"
                            }
                        }
                        }
                    
                
blog
CW
blog
CW
blog
CW

Copilot json

                    



                        {
                        "servers": {
                            "github": {
                            "url": "https://api.githubcopilot.com/mcp/"
                            },
                            "company-documents": {
                            "command": "docker",
                            "args": [
                                "run",
                                "-i",
                                "--rm",
                                "-v",
                                "C:\\Users\\Cezary\\mcp-documents-data:/data",
                                "mcp-server-documents:latest"
                            ]
                            },
                            "company-documents-server": {
                            "transport": "sse",
                            "url": "http://localhost:3001/mcp/sse"
                            }

                        },
                        "inputs": []
                        }
                    
                
blog
CW
blog
CW
blog
CW

YAML główna konfiguracja Continue

                    
                        name: Local Agent
                        version: 1.0.0
                        schema: v1
                        models:
                        - name: Autodetect
                            provider: ollama
                            model: AUTODETECT
                        mcpServers:
                        - name: company-documents
                            command: docker
                            args:
                            - exec
                            - -i
                            - mcp-server-documents-container
                            - ./MCPServerDocuments
                            env: {}
                        - name: company-documents-server
                            type: streamable-http
                            url: http://localhost:3001/mcp

                    
                
blog
CW
blog
CW

LLMStudio json

                    










                    
                        {
                        "mcpServers": {
                            "MCP_DOCKER": {
                            "command": "docker",
                            "args": [
                                "mcp",
                                "gateway",
                                "run"
                            ],
                            "env": {
                                "LOCALAPPDATA": "C:\\Users\\Cezary\\AppData\\Local",
                                "ProgramData": "C:\\ProgramData",
                                "ProgramFiles": "C:\\Program Files"
                            }
                            },
                            "company-documents": {
                            "command": "docker",
                            "args": [
                                "run",
                                "--rm",
                                "-i",
                                "-v",
                                "C:\\Users\\Cezary\\mcp-documents-data:/data",
                                "mcp-server-documents:latest"
                            ]
                            },
                            "company-documents-server": {
                            "url": "http://localhost:3001/mcp"
                            }
                        }
                        }
                    
                
blog
CW
Jako użytkownik Claude PRO mogę spróbować innaczej to zrobić
blog
CW
blog
CW
blog
CW
blog
CW
Spoiler
blog
CW
blog
CW
blog
CW
Jak przetestować nasz kod serwera HTTP/SSE MCP w C#?
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
Jak obecnie wygląda nasz kod serwera MCP w C#?
🤔
blog
CW

Program.cs dla MCP serwer HTTP/SSE

                    






































                        using MCPServerDocuments;
                        using MCPServerDocuments.Database;
                        using MCPServerDocuments.Tools;
                        using Microsoft.Extensions.Options;

                        var builder = WebApplication.CreateBuilder(args);

                        // MCP Server with BOTH transports
                        builder.Services
                            .AddMcpServer()   
                            .WithHttpTransport()     
                            .WithTools<MCPServerDataExportTools>();

                        // Database
                        var defaultDbPath = Path.Combine(
                            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
                            "MCPServerDocuments",
                            "documents.db");

                        builder.Services.AddSingleton(new DatabaseConfig
                        {
                            ConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING") ?? defaultDbPath
                        });

                        // Repositories & services
                        builder.Services.AddSingleton<CompanyDocumentsRepository>();
                        builder.Services.AddSingleton<UserHistoryRepository>();
                        builder.Services.AddSingleton<ExcelService>();

                        //builder.Services.AddSingleton(new MailgunConfig
                        //{
                        //    ApiKey = Environment.GetEnvironmentVariable("MAILGUN_API_KEY") ?? "",
                        //    Domain = Environment.GetEnvironmentVariable("MAILGUN_DOMAIN") ?? "",
                        //    FromEmail = Environment.GetEnvironmentVariable("MAILGUN_FROM_EMAIL") ?? "",
                        //    FromName = Environment.GetEnvironmentVariable("MAILGUN_FROM_NAME") ?? "",
                        //    DefaultRecipient = Environment.GetEnvironmentVariable("DEFAULT_RECIPIENT") ?? ""
                        //});
                        builder.Services.Configure<MailgunConfig>(builder.Configuration.GetSection("Mailgun"));
                        builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<MailgunConfig>>().Value);

                        builder.Services.AddSingleton<MailgunEmailService>();

                        var app = builder.Build();

                        // Initialize database
                        var dbConfig = app.Services.GetRequiredService<DatabaseConfig>();
                        await DatabaseInitializer.InitializeDatabaseAsync(dbConfig.ConnectionString);

                        // Map MCP endpoints
                        app.MapMcp("/mcp"); // Automatycznie mapuje /mcp/sse i mcp/message

                        // health check endpoint
                        app.MapGet("/health", () => Results.Ok(new { 
                            status = "healthy", 
                            timestamp = DateTime.UtcNow,
                            transports = new[] { "sse", "http" }
                        }));

                        app.Run();


                    
                

MCPServerDataExportTools w wersji 0.8.0

                    




















































































































                        using MCPServerDocuments.Database;
                        using MCPServerDocuments.DataClasses;
                        using MCPServerDocuments.Tools;
                        using ModelContextProtocol.Server;
                        using System.ComponentModel;
                        using System.Text.Json;

                        namespace MCPServerDocuments;

                        [McpServerToolType]
                        public sealed class MCPServerDataExportTools(
                            CompanyDocumentsRepository repository,
                            UserHistoryRepository historyRepository,
                            ExcelService excelService,
                            MailgunEmailService mailgunEmailService)
                        {
                            // Using JsonSerializerOptions with web defaults — 
                            // camelCase, no need for source-gen context attributes.
                            // The SDK serializes tool return values automatically, 
                            // but since we return string we control it ourselves.
                            private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
                            {
                                WriteIndented = false
                            };

                            [McpServerTool, Description("Search for documents by PESEL number.")]
                            public async Task<string> SearchDocumentsByPesel(
                                [Description("PESEL number to search for")] string pesel)
                            {
                                try
                                {
                                    var documents = await repository.GetDocumentsByPeselAsync(pesel);

                                    return Serialize(new SearchDocumentsResult
                                    {
                                        Success = true,
                                        Message = $"Found {documents.Count} document(s) for PESEL: {pesel}",
                                        Pesel = pesel,
                                        DocumentCount = documents.Count,
                                        Documents = documents
                                    });
                                }
                                catch (Exception ex)
                                {
                                    return SerializeError(ex.Message);
                                }
                            }

                            [McpServerTool, Description("Search for documents by Person ID.")]
                            public async Task<string> SearchDocumentsByPersonId(
                                [Description("Person ID to search for")] int personId)
                            {
                                try
                                {
                                    var documents = await repository.GetDocumentsByPersonIdAsync(personId);

                                    return Serialize(new SearchDocumentsResult
                                    {
                                        Success = true,
                                        Message = $"Found {documents.Count} document(s) for Person ID: {personId}",
                                        PersonId = personId,
                                        DocumentCount = documents.Count,
                                        Documents = documents
                                    });
                                }
                                catch (Exception ex)
                                {
                                    return SerializeError(ex.Message);
                                }
                            }

                            [McpServerTool, Description("Get all user history with full details including user names and document information.")]
                            public async Task<string> GetAllUserHistoryWithDetails()
                            {
                                try
                                {
                                    var historyDetails = await historyRepository.GetAllHistoryWithDetailsAsync();

                                    return Serialize(new UserHistoryResult
                                    {
                                        Success = true,
                                        Message = $"Found {historyDetails.Count} history record(s)",
                                        RecordCount = historyDetails.Count,
                                        HistoryRecords = historyDetails
                                    });
                                }
                                catch (Exception ex)
                                {
                                    return SerializeError(ex.Message);
                                }
                            }

                            [McpServerTool, Description("Search by Person ID, export to Excel in-memory, and send via email to recipient.")]
                            public async Task<string> ProcessDocumentWorkflowByPersonId(
                                [Description("Person ID to search for")] int personId,
                                [Description("Email recipient (optional)")] string? recipient = null)
                            {
                                try
                                {
                                    var documents = await repository.GetDocumentsByPersonIdAsync(personId);
                                    if (documents.Count == 0)
                                        return SerializeError($"No documents found for Person ID: {personId}");

                                    var fileBytes = await excelService.SaveDocumentsAsExcelAsync(documents, $"PersonID_{personId}");
                                    var fileName = $"PersonID_{personId}_export.xlsx";

                                    var mailgunMessageId = await mailgunEmailService.SendExcelEmailAsync(
                                        fileBytes, fileName, recipient,
                                        $"Document Export for Person ID: {personId}",
                                        $"Please find attached {documents.Count} document(s) for Person ID {personId}.");

                                    return Serialize(new WorkflowResult
                                    {
                                        Success = true,
                                        Message = "Complete workflow executed successfully",
                                        PersonId = personId,
                                        DocumentCount = documents.Count,
                                        FileName = fileName,
                                        EmailSentTo = recipient ?? "default recipient",
                                        EmailService = "Mailgun",
                                        MailgunMessageId = mailgunMessageId
                                    });
                                }
                                catch (Exception ex)
                                {
                                    return SerializeError(ex.Message);
                                }
                            }

                            // Helpers

                            private static string Serialize<T>(T value) =>
                                JsonSerializer.Serialize(value, JsonOptions);

                            private static string SerializeError(string error) =>
                                Serialize(new ErrorResult { Success = false, Error = error });
                        }


                    
                

Klasy typów zwracanych, ale w sumie nie są potrzebne

                    




























                        using MCPServerDocuments.DataClasses;

                        namespace MCPServerDocuments.DataClasses;

                        // Search results

                        public class SearchDocumentsResult
                        {
                            public bool Success { get; set; }
                            public string Message { get; set; } = string.Empty;
                            public string? Pesel { get; set; }
                            public int? PersonId { get; set; }
                            public int DocumentCount { get; set; }
                            public List<CompanyDocument> Documents { get; set; } = [];
                            public DateTime Timestamp { get; set; } = DateTime.UtcNow;
                        }

                        // User history results

                        public class UserHistoryResult
                        {
                            public bool Success { get; set; }
                            public string Message { get; set; } = string.Empty;
                            public int RecordCount { get; set; }
                            public List<UserHistoryWithDetails> HistoryRecords { get; set; } = [];
                            public DateTime Timestamp { get; set; } = DateTime.UtcNow;
                        }

                        // Excel + email workflow results
                        public class WorkflowResult
                        {
                            public bool Success { get; set; }
                            public string Message { get; set; } = string.Empty;
                            public int? PersonId { get; set; }
                            public int DocumentCount { get; set; }
                            public string FileName { get; set; } = string.Empty;
                            public string EmailSentTo { get; set; } = string.Empty;
                            public string EmailService { get; set; } = string.Empty;
                            public string? MailgunMessageId { get; set; }
                            public DateTime Timestamp { get; set; } = DateTime.UtcNow;
                        }

                        // Generic error

                        public class ErrorResult
                        {
                            public bool Success { get; set; }
                            public string Error { get; set; } = string.Empty;
                            public DateTime Timestamp { get; set; } = DateTime.UtcNow;
                        }

                    
                

Przykład ExcelService

                    








































                        using MCPServerDocuments.DataClasses;
                        using OfficeOpenXml;
                        using OfficeOpenXml.Style;
                        using System.Drawing;

                        namespace MCPServerDocuments.Tools;

                        public class ExcelService
                        {
                            public Task<byte[]> SaveDocumentsAsExcelAsync(List<CompanyDocument> documents, string fileNamePrefix)
                            {
                                ExcelPackage.LicenseContext = LicenseContext.NonCommercial;

                                using var package = new ExcelPackage();
                                var ws = package.Workbook.Worksheets.Add("Document Export");

                                // Headers
                                string[] headers = ["ID", "Person ID", "PESEL", "Document Type", "Document Number",
                                                    "Title", "Description", "File Path", "Created At", "Modified At", "Status"];

                                for (int col = 0; col < headers.Length; col++)
                                    ws.Cells[1, col + 1].Value = headers[col];

                                // Data rows
                                for (int i = 0; i < documents.Count; i++)
                                {
                                    var d = documents[i];
                                    var row = i + 2;
                                    ws.Cells[row, 1].Value = d.Id;
                                    ws.Cells[row, 2].Value = d.PersonId;
                                    ws.Cells[row, 3].Value = d.Pesel;
                                    ws.Cells[row, 4].Value = d.DocumentType;
                                    ws.Cells[row, 5].Value = d.DocumentNumber;
                                    ws.Cells[row, 6].Value = d.Title;
                                    ws.Cells[row, 7].Value = d.Description;
                                    ws.Cells[row, 8].Value = d.FilePath;
                                    ws.Cells[row, 9].Value = d.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss");
                                    ws.Cells[row, 10].Value = d.ModifiedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? "";
                                    ws.Cells[row, 11].Value = d.Status;
                                }

                                FormatWorksheet(ws, headers.Length, documents.Count);

                                using var stream = new MemoryStream();
                                package.SaveAs(stream);
                                return Task.FromResult(stream.ToArray());
                            }

                            private static void FormatWorksheet(ExcelWorksheet ws, int columnCount, int rowCount)
                            {
                                using (var range = ws.Cells[1, 1, 1, columnCount])
                                {
                                    range.Style.Font.Bold = true;
                                    range.Style.Fill.PatternType = ExcelFillStyle.Solid;
                                    range.Style.Fill.BackgroundColor.SetColor(Color.LightGray);
                                    range.Style.Border.BorderAround(ExcelBorderStyle.Thin);
                                }

                                for (int col = 1; col <= columnCount; col++)
                                    ws.Column(col).AutoFit();

                                if (rowCount > 0)
                                {
                                    using var range = ws.Cells[2, 1, rowCount + 1, columnCount];
                                    range.Style.Border.BorderAround(ExcelBorderStyle.Thin);
                                    range.Style.Border.Bottom.Style = ExcelBorderStyle.Thin;
                                }
                            }
                        }

                    
                

Przykład MailGunService

                    






































































                        using Microsoft.Extensions.Logging;
                        using System.Net.Http.Headers;
                        using System.Text;

                        namespace MCPServerDocuments.Tools;

                        public class MailgunEmailService
                        {
                            private readonly MailgunConfig _config;
                            private readonly ILogger<MailgunEmailService> _logger;
                            private readonly HttpClient _httpClient;

                            public MailgunEmailService(MailgunConfig config, ILogger<MailgunEmailService> logger)
                            {
                                _config = config ?? throw new ArgumentNullException(nameof(config));
                                _logger = logger ?? throw new ArgumentNullException(nameof(logger));

                                if (string.IsNullOrWhiteSpace(config.ApiKey))
                                    throw new InvalidOperationException("Mailgun API key is not configured.");
                                if (string.IsNullOrWhiteSpace(config.Domain))
                                    throw new InvalidOperationException("Mailgun domain is not configured.");

                                _httpClient = new HttpClient();
                                var authToken = Convert.ToBase64String(Encoding.ASCII.GetBytes($"api:{config.ApiKey}"));
                                _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authToken);
                            }

                            /// <summary>
                            /// Sends an email with an in-memory Excel file attachment via Mailgun.
                            /// </summary>
                            public async Task<string> SendExcelEmailAsync(
                                byte[] fileBytes,
                                string fileName,
                                string? recipient = null,
                                string? subject = null,
                                string? messageBody = null)
                            {
                                var toEmail = !string.IsNullOrWhiteSpace(recipient) ? recipient : _config.DefaultRecipient;

                                if (string.IsNullOrWhiteSpace(toEmail))
                                    return "No recipient email address provided and no default configured.";

                                var emailSubject = subject ?? "Document Export Attached";
                                var emailContent = messageBody ?? "Please find the requested document export attached.";

                                using var form = new MultipartFormDataContent
                                {
                                    { new StringContent($"{_config.FromName} <{_config.FromEmail}>"), "from" },
                                    { new StringContent(toEmail), "to" },
                                    { new StringContent(emailSubject), "subject" },
                                    { new StringContent(emailContent), "text" }
                                };

                                var fileContent = new ByteArrayContent(fileBytes);
                                fileContent.Headers.ContentType = new MediaTypeHeaderValue(
                                    "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
                                form.Add(fileContent, "attachment", fileName);

                                var url = $"https://api.mailgun.net/v3/{_config.Domain}/messages";
                                var response = await _httpClient.PostAsync(url, form);
                                var responseBody = await response.Content.ReadAsStringAsync();

                                if (!response.IsSuccessStatusCode)
                                {
                                    _logger.LogError("Failed to send email via Mailgun. Status: {StatusCode}, Error: {Error}",
                                        response.StatusCode, responseBody);
                                    return $"Mailgun email failed: {responseBody}";
                                }

                                var messageId = ExtractMessageId(responseBody) ?? "unknown";
                                _logger.LogInformation("Email sent via Mailgun. Message-ID: {MessageId}, To: {To}", messageId, toEmail);
                                return messageId;
                            }

                            private static string? ExtractMessageId(string responseBody)
                            {
                                var marker = "\"id\":";
                                var idx = responseBody.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
                                if (idx >= 0)
                                {
                                    var start = responseBody.IndexOf('<', idx);
                                    var end = responseBody.IndexOf('>', start);
                                    if (start >= 0 && end > start)
                                        return responseBody[start..(end + 1)];
                                }
                                return null;
                            }
                        }

                        public class MailgunConfig
                        {
                            public string ApiKey { get; set; } = string.Empty;
                            public string Domain { get; set; } = string.Empty;
                            public string FromEmail { get; set; } = string.Empty;
                            public string FromName { get; set; } = string.Empty;
                            public string DefaultRecipient { get; set; } = string.Empty;
                        }

                    
                

CompanyDocumentsRepository

                    












































































                        using MCPServerDocuments.DataClasses;
                        using Microsoft.Data.Sqlite;

                        namespace MCPServerDocuments.Database;

                        public class CompanyDocumentsRepository(DatabaseConfig dbConfig)
                        {
                            public async Task<List<CompanyDocument>> GetDocumentsByPeselAsync(string pesel)
                            {
                                using var connection = CreateConnection();
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = """
                                    SELECT Id, PersonId, Pesel, DocumentType, DocumentNumber, Title, 
                                        Description, FilePath, CreatedAt, ModifiedAt, Status
                                    FROM Documents
                                    WHERE Pesel = $pesel
                                    ORDER BY CreatedAt DESC
                                    """;
                                command.Parameters.AddWithValue("$pesel", pesel);

                                return await ReadDocumentsAsync(command);
                            }

                            public async Task<List<CompanyDocument>> GetDocumentsByPersonIdAsync(int personId)
                            {
                                using var connection = CreateConnection();
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = """
                                    SELECT Id, PersonId, Pesel, DocumentType, DocumentNumber, Title, 
                                        Description, FilePath, CreatedAt, ModifiedAt, Status
                                    FROM Documents
                                    WHERE PersonId = $personId
                                    ORDER BY CreatedAt DESC
                                    """;
                                command.Parameters.AddWithValue("$personId", personId);

                                return await ReadDocumentsAsync(command);
                            }

                            public async Task<int> AddDocumentAsync(CompanyDocument document)
                            {
                                using var connection = CreateConnection();
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = """
                                    INSERT INTO Documents (PersonId, Pesel, DocumentType, DocumentNumber, Title, 
                                                        Description, FilePath, CreatedAt, Status)
                                    VALUES ($personId, $pesel, $docType, $docNumber, $title, $description, 
                                            $filePath, $createdAt, $status);
                                    SELECT last_insert_rowid();
                                    """;

                                command.Parameters.AddWithValue("$personId", document.PersonId);
                                command.Parameters.AddWithValue("$pesel", document.Pesel);
                                command.Parameters.AddWithValue("$docType", document.DocumentType);
                                command.Parameters.AddWithValue("$docNumber", document.DocumentNumber);
                                command.Parameters.AddWithValue("$title", document.Title);
                                command.Parameters.AddWithValue("$description", document.Description);
                                command.Parameters.AddWithValue("$filePath", document.FilePath);
                                command.Parameters.AddWithValue("$createdAt", document.CreatedAt);
                                command.Parameters.AddWithValue("$status", document.Status);

                                var result = await command.ExecuteScalarAsync();
                                return Convert.ToInt32(result);
                            }

                            private SqliteConnection CreateConnection() =>
                                new($"Data Source={dbConfig.ConnectionString}");

                            private static async Task<List<CompanyDocument>> ReadDocumentsAsync(SqliteCommand command)
                            {
                                var documents = new List<CompanyDocument>();
                                using var reader = await command.ExecuteReaderAsync();
                                while (await reader.ReadAsync())
                                {
                                    documents.Add(new CompanyDocument
                                    {
                                        Id = reader.GetInt32(0),
                                        PersonId = reader.GetInt32(1),
                                        Pesel = reader.GetString(2),
                                        DocumentType = reader.GetString(3),
                                        DocumentNumber = reader.GetString(4),
                                        Title = reader.GetString(5),
                                        Description = reader.GetString(6),
                                        FilePath = reader.GetString(7),
                                        CreatedAt = reader.GetDateTime(8),
                                        ModifiedAt = reader.IsDBNull(9) ? null : reader.GetDateTime(9),
                                        Status = reader.GetString(10)
                                    });
                                }
                                return documents;
                            }
                        }

                    
                

UserHistoryRepository

                    



























































































































































































































                        using MCPServerDocuments.DataClasses;
                        using Microsoft.Data.Sqlite;

                        namespace MCPServerDocuments.Database;

                        public class UserHistoryRepository(DatabaseConfig dbConfig)
                        {
                            // User Methods

                            public async Task<User?> GetUserByIdAsync(int userId)
                            {
                                using var connection = CreateConnection();
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = "SELECT Id, UserName FROM Users WHERE Id = $userId";
                                command.Parameters.AddWithValue("$userId", userId);

                                using var reader = await command.ExecuteReaderAsync();
                                return await reader.ReadAsync() ? MapUser(reader) : null;
                            }

                            public async Task<User?> GetUserByNameAsync(string userName)
                            {
                                using var connection = CreateConnection();
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = "SELECT Id, UserName FROM Users WHERE UserName = $userName";
                                command.Parameters.AddWithValue("$userName", userName);

                                using var reader = await command.ExecuteReaderAsync();
                                return await reader.ReadAsync() ? MapUser(reader) : null;
                            }

                            public async Task<List<User>> GetAllUsersAsync()
                            {
                                using var connection = CreateConnection();
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = "SELECT Id, UserName FROM Users ORDER BY UserName";

                                var users = new List<User>();
                                using var reader = await command.ExecuteReaderAsync();
                                while (await reader.ReadAsync())
                                    users.Add(MapUser(reader));
                                return users;
                            }

                            public async Task<int> AddUserAsync(User user)
                            {
                                using var connection = CreateConnection();
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = """
                                    INSERT INTO Users (UserName) VALUES ($userName);
                                    SELECT last_insert_rowid();
                                    """;
                                command.Parameters.AddWithValue("$userName", user.UserName);

                                var result = await command.ExecuteScalarAsync();
                                return Convert.ToInt32(result);
                            }

                            // History Methods

                            public async Task<List<UserHistoryUsingCompanyDocuments>> GetHistoryByUserIdAsync(int userId)
                            {
                                using var connection = CreateConnection();
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = """
                                    SELECT Id, UserId, UserQuery, Action, DocumentId, CreatedAt
                                    FROM UserHistoryUsingCompanyDocuments
                                    WHERE UserId = $userId
                                    ORDER BY CreatedAt DESC
                                    """;
                                command.Parameters.AddWithValue("$userId", userId);

                                return await ReadHistoryAsync(command);
                            }

                            public async Task<List<UserHistoryUsingCompanyDocuments>> GetHistoryByDocumentIdAsync(int documentId)
                            {
                                using var connection = CreateConnection();
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = """
                                    SELECT Id, UserId, UserQuery, Action, DocumentId, CreatedAt
                                    FROM UserHistoryUsingCompanyDocuments
                                    WHERE DocumentId = $documentId
                                    ORDER BY CreatedAt DESC
                                    """;
                                command.Parameters.AddWithValue("$documentId", documentId);

                                return await ReadHistoryAsync(command);
                            }

                            public async Task<List<UserHistoryUsingCompanyDocuments>> GetHistoryByActionAsync(string action)
                            {
                                using var connection = CreateConnection();
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = """
                                    SELECT Id, UserId, UserQuery, Action, DocumentId, CreatedAt
                                    FROM UserHistoryUsingCompanyDocuments
                                    WHERE Action = $action
                                    ORDER BY CreatedAt DESC
                                    """;
                                command.Parameters.AddWithValue("$action", action);

                                return await ReadHistoryAsync(command);
                            }

                            public async Task<int> AddHistoryAsync(UserHistoryUsingCompanyDocuments history)
                            {
                                using var connection = CreateConnection();
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = """
                                    INSERT INTO UserHistoryUsingCompanyDocuments (UserId, UserQuery, Action, DocumentId, CreatedAt)
                                    VALUES ($userId, $userQuery, $action, $documentId, $createdAt);
                                    SELECT last_insert_rowid();
                                    """;

                                command.Parameters.AddWithValue("$userId", history.UserId);
                                command.Parameters.AddWithValue("$userQuery", history.UserQuery);
                                command.Parameters.AddWithValue("$action", history.Action);
                                command.Parameters.AddWithValue("$documentId", history.DocumentId);
                                command.Parameters.AddWithValue("$createdAt", history.CreatedAt);

                                var result = await command.ExecuteScalarAsync();
                                return Convert.ToInt32(result);
                            }

                            // Joined Queries

                            private const string HistoryWithDetailsQuery = """
                                SELECT 
                                    h.Id as HistoryId, h.UserId, u.UserName, h.UserQuery, h.Action,
                                    h.DocumentId, d.Title as DocumentTitle, d.DocumentType, d.DocumentNumber, h.CreatedAt
                                FROM UserHistoryUsingCompanyDocuments h
                                INNER JOIN Users u ON h.UserId = u.Id
                                INNER JOIN Documents d ON h.DocumentId = d.Id
                                """;

                            public async Task<List<UserHistoryWithDetails>> GetHistoryWithDetailsByUserIdAsync(int userId)
                            {
                                using var connection = CreateConnection();
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = $"{HistoryWithDetailsQuery} WHERE h.UserId = $userId ORDER BY h.CreatedAt DESC";
                                command.Parameters.AddWithValue("$userId", userId);

                                return await ReadHistoryWithDetailsAsync(command);
                            }

                            public async Task<List<UserHistoryWithDetails>> GetHistoryWithDetailsByDocumentIdAsync(int documentId)
                            {
                                using var connection = CreateConnection();
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = $"{HistoryWithDetailsQuery} WHERE h.DocumentId = $documentId ORDER BY h.CreatedAt DESC";
                                command.Parameters.AddWithValue("$documentId", documentId);

                                return await ReadHistoryWithDetailsAsync(command);
                            }

                            public async Task<List<UserHistoryWithDetails>> GetAllHistoryWithDetailsAsync()
                            {
                                using var connection = CreateConnection();
                                await connection.OpenAsync();

                                var command = connection.CreateCommand();
                                command.CommandText = $"{HistoryWithDetailsQuery} ORDER BY h.CreatedAt DESC";

                                return await ReadHistoryWithDetailsAsync(command);
                            }

                            // Helpers

                            private SqliteConnection CreateConnection() =>
                                new($"Data Source={dbConfig.ConnectionString}");

                            private static User MapUser(SqliteDataReader reader) => new()
                            {
                                Id = reader.GetInt32(0),
                                UserName = reader.GetString(1)
                            };

                            private static async Task<List<UserHistoryUsingCompanyDocuments>> ReadHistoryAsync(SqliteCommand command)
                            {
                                var history = new List<UserHistoryUsingCompanyDocuments>();
                                using var reader = await command.ExecuteReaderAsync();
                                while (await reader.ReadAsync())
                                {
                                    history.Add(new UserHistoryUsingCompanyDocuments
                                    {
                                        Id = reader.GetInt32(0),
                                        UserId = reader.GetInt32(1),
                                        UserQuery = reader.GetString(2),
                                        Action = reader.GetString(3),
                                        DocumentId = reader.GetInt32(4),
                                        CreatedAt = reader.GetDateTime(5)
                                    });
                                }
                                return history;
                            }

                            private static async Task<List<UserHistoryWithDetails>> ReadHistoryWithDetailsAsync(SqliteCommand command)
                            {
                                var history = new List<UserHistoryWithDetails>();
                                using var reader = await command.ExecuteReaderAsync();
                                while (await reader.ReadAsync())
                                {
                                    history.Add(new UserHistoryWithDetails
                                    {
                                        HistoryId = reader.GetInt32(0),
                                        UserId = reader.GetInt32(1),
                                        UserName = reader.GetString(2),
                                        UserQuery = reader.GetString(3),
                                        Action = reader.GetString(4),
                                        DocumentId = reader.GetInt32(5),
                                        DocumentTitle = reader.GetString(6),
                                        DocumentType = reader.GetString(7),
                                        DocumentNumber = reader.GetString(8),
                                        CreatedAt = reader.GetDateTime(9)
                                    });
                                }
                                return history;
                            }
                        }

                    
                
Postanowiłem ułożyć cały projekt od początku
blog
CW
blog
CW
Rozwiążmy teraz problem z użytkownikami
  1. Co chcemy?
Od wersji 0.8.0 jest to możliwe
Powiedzmy, że mamy 2 role: Employee i Manager

UserRole i UserContext

                    


                        namespace CompanyDocuments.Common.Services;

                        public enum UserRole
                        {
                            Employee,
                            Manager
                        }

                        /// <summary>
                        /// Represents the authenticated user making the MCP request.
                        /// Resolved from the Authorization header (Bearer token).
                        /// </summary>
                        public sealed class UserContext
                        {
                            public required string UserId { get; init; }
                            public required string Name { get; init; }
                            public required UserRole Role { get; init; }
                        }

                    
                
Samo sprawdzenie, czy użytkownik ma dostęp do danych, jest stosunkowo proste
blog
CW
blog
CW
blog
CW
blog
CW

ReportService

                    
















































































                        namespace CompanyDocuments.Common.Services;

                        public sealed class ReportService
                        {
                        // Static report registry — each report has required role
                        private static readonly Dictionary<string, ReportDefinition> Reports = 
                            new(StringComparer.OrdinalIgnoreCase)
                        {
                            ["financial-q1"] = new(
                                "financial-q1",
                                "Raport finansowy Q1 2025",
                                UserRole.Manager,
                                "Przychody: 334 500 PLN | Koszty: 198 700 PLN | Zysk: 135 800 PLN | Marża: 40.6%"),

                            ["financial-q2"] = new(
                                "financial-q2",
                                "Raport finansowy Q2 2025",
                                UserRole.Manager,
                                "Przychody: 357 000 PLN | Koszty: 224 500 PLN | Zysk: 132 500 PLN | Marża: 37.1%"),

                            ["salary-report"] = new(
                                "salary-report",
                                "Zestawienie wynagrodzeń",
                                UserRole.Manager,
                                "Średnia: 14 200 PLN | Mediana: 12 800 PLN | Min: 7 500 PLN | Max: 28 000 PLN"),

                            ["team-tasks"] = new(
                                "team-tasks",
                                "Zadania zespołu — podsumowanie",
                                UserRole.Employee,
                                "Otwarte: 12 | W trakcie: 8 | Zakończone: 34 | Sprinty: 3"),

                            ["my-timesheet-summary"] = new(
                                "my-timesheet-summary",
                                "Podsumowanie godzin pracy",
                                UserRole.Employee,
                                "Czerwiec 2025 — Przepracowane: 168h | Nadgodziny: 12h | Urlop: 0 dni")
                        };

                        /// <summary>
                        /// Returns all reports with access information for the given user role.
                        /// Reports the user cannot access are still listed — but marked as denied.
                        /// </summary>
                        public Task<List<ReportAccessResult>> GetReportsForUserAsync(UserRole userRole)
                        {
                            var results = Reports.Values.Select(r =>
                            {
                                bool hasAccess = userRole >= r.RequiredRole;

                                return new ReportAccessResult(
                                    ReportId: r.Id,
                                    Title: r.Title,
                                    HasAccess: hasAccess,
                                    Content: hasAccess ? r.Content : null,
                                    DeniedReason: hasAccess
                                        ? null
                                        : $"Brak dostępu. Wymagana rola: {r.RequiredRole}. Twoja rola: {userRole}.");
                            }).ToList();

                            return Task.FromResult(results);
                        }

                        /// <summary>
                        /// Get a single report by ID with access check.
                        /// </summary>
                        public Task<ReportAccessResult> GetReportByIdAsync(string reportId, UserRole userRole)
                        {
                            if (!Reports.TryGetValue(reportId, out var report))
                            {
                                return Task.FromResult(new ReportAccessResult(
                                    ReportId: reportId,
                                    Title: "Nieznany raport",
                                    HasAccess: false,
                                    Content: null,
                                    DeniedReason: $"Raport '{reportId}' nie istnieje."));
                            }

                            bool hasAccess = userRole >= report.RequiredRole;

                            return Task.FromResult(new ReportAccessResult(
                                ReportId: report.Id,
                                Title: report.Title,
                                HasAccess: hasAccess,
                                Content: hasAccess ? report.Content : null,
                                DeniedReason: hasAccess
                                    ? null
                                    : $"Raport '{report.Title}' istnieje, ale nie masz do niego dostępu. 
                                    Wymagana rola: {report.RequiredRole}. Twoja rola: {userRole}."));
                        }
                        }

                        public sealed record ReportDefinition(
                        string Id,
                        string Title,
                        UserRole RequiredRole,
                        string Content);

                        public sealed record ReportAccessResult(
                        string ReportId,
                        string Title,
                        bool HasAccess,
                        string? Content,
                        string? DeniedReason);

                    
                

ReportTools pośrednik w [McpServerToolType]

                    





















                        //  REPORT TOOLS — [RequireRole(Employee)]
                        //  Visible to everyone authenticated.
                        //  The tool itself shows all reports, but content is role-gated.

                        [McpServerToolType]
                        [RequireRole(UserRole.Employee)]
                        public sealed class ReportTools(
                            IHttpContextAccessor httpContextAccessor,
                            ReportService reportService)
                        {
                            private UserContext User => 
                                (httpContextAccessor.HttpContext?.Items["UserContext"] as UserContext)!;

                            [McpServerTool, Description(
                                "List all available reports. Shows which reports you have access to " +
                                "and which are restricted based on your role.")]
                            public async Task<string> GetReports()
                            {
                                var reports = await reportService.GetReportsForUserAsync(User.Role);
                                return McpJson.Serialize(new
                                {
                                    success = true,
                                    user = User.Name,
                                    role = User.Role.ToString(),
                                    totalReports = reports.Count,
                                    accessibleReports = reports.Count(r => r.HasAccess),
                                    deniedReports = reports.Count(r => !r.HasAccess),
                                    reports
                                });
                            }

                            [McpServerTool, Description(
                                "Get a specific report by ID. Returns content if authorized, " +
                                "or an access-denied message with the required role.")]
                            public async Task<string> GetReportById(
                                [Description("Report ID (e.g. financial-q1, team-tasks, salary-report)")] string reportId)
                            {
                                var report = await reportService.GetReportByIdAsync(reportId, User.Role);
                                return McpJson.Serialize(new
                                {
                                    success = report.HasAccess,
                                    user = User.Name,
                                    role = User.Role.ToString(),
                                    report
                                });
                            }
                        }

                    
                
blog
CW
Oto nasze wyzwanie z MCP
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
Jak chcemy aby to działało?
blog
CW
blog
CW
blog
CW
Oto jak to zrobić w kodzie
Oto nasze serwisy

EmployeeService , FinancialService , SalesService , TaskService

                    
























































                        







                        public sealed class EmployeeService
                        {
                            public Task<List<Employee>> GetEmployeeListAsync() =>
                                Task.FromResult<List<Employee>>(
                                [
                                    new(1, "Jan Nowak",      "Developer",        "IT"),
                                    new(2, "Maria Wiśniewska", "QA Engineer",    "IT"),
                                    new(3, "Piotr Zieliński", "Project Manager", "PMO"),
                                    new(4, "Katarzyna Dąbrowska", "HR Specialist", "HR")
                                ]);
                        }

                        public sealed record Employee(
                            int Id,
                            string Name,
                            string Position,
                            string Department);


                        public sealed class FinancialService
                        {
                            public Task<FinancialSummary> GetFinancialSummaryAsync() =>
                                Task.FromResult(new FinancialSummary(
                                    Revenue: 691_500m,
                                    Costs: 423_200m,
                                    Profit: 268_300m,
                                    Currency: "PLN",
                                    Period: "H1-2025"));
                        }

                        public sealed record FinancialSummary(
                            decimal Revenue,
                            decimal Costs,
                            decimal Profit,
                            string Currency,
                            string Period);

                        public sealed class SalesService
                        {
                            public Task<List<SaleRecord>> GetSalesReportAsync() =>
                                Task.FromResult<List<SaleRecord>>(
                                [
                                    new("Q1-2025", "Licencje Enterprise", 245_000m, "PLN"),
                                    new("Q1-2025", "Usługi konsultingowe", 89_500m, "PLN"),
                                    new("Q2-2025", "Licencje Enterprise", 312_000m, "PLN"),
                                    new("Q2-2025", "Szkolenia", 45_000m, "PLN")
                                ]);
                        }

                        public sealed record SaleRecord(
                            string Period,
                            string Product,
                            decimal Amount,
                            string Currency);



                        public sealed class TaskService
                        {
                            // In real app: filter by userId from DB
                            public Task<List<WorkTask>> GetTasksForUserAsync(string userId) =>
                                Task.FromResult<List<WorkTask>>(
                                [
                                    new(101, "Przygotować prezentację MCP",   "InProgress", "2025-06-15"),
                                    new(102, "Code review PR #247",            "Todo",       "2025-06-12"),
                                    new(103, "Aktualizacja dokumentacji API",  "Done",       "2025-06-10")
                                ]);

                            public Task<TimesheetConfirmation> SubmitTimesheetAsync(
                                string userId, string date, decimal hours, string description) =>
                                Task.FromResult(new TimesheetConfirmation(
                                    Id: Guid.NewGuid().ToString("N")[..8],
                                    UserId: userId,
                                    Date: date,
                                    Hours: hours,
                                    Description: description,
                                    Status: "Submitted"));
                        }

                        public sealed record WorkTask(
                            int Id,
                            string Title,
                            string Status,
                            string DueDate);

                        public sealed record TimesheetConfirmation(
                            string Id,
                            string UserId,
                            string Date,
                            decimal Hours,
                            string Description,
                            string Status);
                    
                
Tworzymy atrybut, który będzie nam mówił jakie role są wymagane do korzystania z narzędzia

RequireRoleAttribute

                    

                        /// <summary>
                        /// Marks an MCP tool class as requiring a specific role.
                        /// Used by RoleBasedToolProvider to filter tools per user.
                        /// </summary>
                        [AttributeUsage(AttributeTargets.Class, Inherited = false)]
                        public sealed class RequireRoleAttribute(UserRole minimumRole) 
                            : Attribute
                        {
                            public UserRole MinimumRole { get; } = minimumRole;
                        }

                    
                
Oto użycie tego atrybutu
w praktyce

MCP wydzielone w zależności od roli : ManagerTools, EmployeeTools

                    



























































                        //  MANAGER TOOLS — [RequireRole(Manager)]
                        //  These tools are completely invisible to Employee users.
                        //  They won't even show up in tools/list.

                        [McpServerToolType]
                        [RequireRole(UserRole.Manager)]
                        public sealed class ManagerTools(
                            IHttpContextAccessor httpContextAccessor,
                            SalesService salesService,
                            EmployeeService employeeService,
                            FinancialService financialService)
                        {
                            private UserContext User => (httpContextAccessor.HttpContext?.Items["UserContext"] as UserContext)!;

                            [McpServerTool, Description("Get sales report with revenue data per quarter and product.")]
                            public async Task<string> GetSalesReport()
                            {
                                var data = await salesService.GetSalesReportAsync();
                                return McpJson.Serialize
                                (new { success = true, user = User.Name, report = "Sales Report", records = data });
                            }

                            [McpServerTool, Description("Get list of all employees with their positions and departments.")]
                            public async Task<string> GetEmployeeList()
                            {
                                var data = await employeeService.GetEmployeeListAsync();
                                return McpJson.Serialize
                                (new { success = true, user = User.Name, report = "Employee List", employees = data });
                            }

                            [McpServerTool, Description("Get financial summary including revenue, costs and profit.")]
                            public async Task<string> GetFinancialSummary()
                            {
                                var data = await financialService.GetFinancialSummaryAsync();
                                return McpJson.Serialize
                                (new { success = true, user = User.Name, report = "Financial Summary", summary = data });
                            }
                        }

                        //  EMPLOYEE TOOLS — [RequireRole(Employee)]
                        //  Visible to Employee AND Manager (Manager >= Employee).

                        [McpServerToolType]
                        [RequireRole(UserRole.Employee)]
                        public sealed class EmployeeTools(
                            IHttpContextAccessor httpContextAccessor,
                            TaskService taskService)
                        {
                            private UserContext User => (httpContextAccessor.HttpContext?.Items["UserContext"] as UserContext)!;

                            [McpServerTool, Description("Get my current tasks and their statuses.")]
                            public async Task<string> GetMyTasks()
                            {
                                var data = await taskService.GetTasksForUserAsync(User.UserId);
                                return McpJson.Serialize
                                (new { success = true, user = User.Name, tasks = data });
                            }

                            [McpServerTool, Description("Submit a timesheet entry with worked hours.")]
                            public async Task<string> SubmitTimesheet(
                                [Description("Date in YYYY-MM-DD format")] string date,
                                [Description("Number of hours worked")] decimal hours,
                                [Description("Description of work performed")] string description)
                            {
                                var result = await taskService.SubmitTimesheetAsync(User.UserId, date, hours, description);
                                return McpJson.Serialize
                                (new { success = true, user = User.Name, confirmation = result });
                            }
                        }

                        //  Shared JSON helper

                        internal static class McpJson
                        {
                            public static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web)
                            {
                                WriteIndented = true
                            };

                            public static string Serialize<T>(T value) =>
                                JsonSerializer.Serialize(value, Options);
                        }


                    
                
Potrzebujemy oczywiście kodu który będzie filtrował narzędzia na podstawie roli użytkownika

RoleBasedToolProvider

                    





















































































                        /// <summary>
                        /// Discovers all [McpServerToolType] classes from the assembly at startup,
                        /// then filters them per-request based on the user's role (from Bearer token).
                        ///
                        /// Tools decorated with [RequireRole(UserRole.Manager)] are only visible to Managers.
                        /// Tools without [RequireRole] are visible to all authenticated users.
                        /// Unauthenticated users see zero tools.
                        ///
                        /// IMPORTANT: Because of issue #707 in csharp-sdk, we cannot use .WithTools<T>()
                        /// together with WithListToolsHandler — registered tools always get appended.
                        /// Instead, we skip .WithTools<T>() entirely and handle discovery + DI ourselves.
                        /// </summary>
                        public sealed class RoleBasedToolProvider
                        {
                            private readonly List<ToolEntry> _allTools;

                            public RoleBasedToolProvider(IServiceProvider serviceProvider)
                            {
                                _allTools = DiscoverTools(serviceProvider);
                            }

                            /// <summary>
                            /// Returns only the tools the current user is authorized to see.
                            /// Called from WithListToolsHandler.
                            /// </summary>
                            public IReadOnlyList<McpServerTool> GetToolsForUser(UserContext? user)
                            {
                                if (user is null)
                                    return [];

                                return _allTools
                                    .Where(t => user.Role >= t.MinimumRole)
                                    .Select(t => t.Tool)
                                    .ToList();
                            }

                            /// <summary>
                            /// Finds a tool by name, but only if the user is authorized.
                            /// Called from WithCallToolHandler.
                            /// </summary>
                            public McpServerTool? FindTool(string toolName, UserContext? user)
                            {
                                if (user is null)
                                    return null;

                                return _allTools
                                    .Where(t => user.Role >= t.MinimumRole)
                                    .FirstOrDefault(t => t.Tool.ProtocolTool.Name == toolName)
                                    ?.Tool;
                            }

                            // Discovery 

                            private static List<ToolEntry> DiscoverTools(IServiceProvider serviceProvider)
                            {
                                var toolAssembly = typeof(RoleBasedToolProvider).Assembly;
                                var entries = new List<ToolEntry>();

                                var options = new McpServerToolCreateOptions
                                {
                                    Services = serviceProvider
                                };

                                var toolTypes = toolAssembly.GetTypes()
                                    .Where(t => t.GetCustomAttribute<McpServerToolTypeAttribute>() is not null);

                                foreach (var type in toolTypes)
                                {
                                    var minimumRole = type.GetCustomAttribute<RequireRoleAttribute>()?.MinimumRole
                                                    ?? UserRole.Employee;

                                    var methods = type.GetMethods(
                                        BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);

                                    foreach (var method in methods)
                                    {
                                        if (method.GetCustomAttribute<McpServerToolAttribute>() is null)
                                            continue;

                                        McpServerTool tool;

                                        if (method.IsStatic)
                                        {
                                            tool = McpServerTool.Create(method, options);
                                        }
                                        else
                                        {
                                            // Use the Func<RequestContext, object> overload so SDK
                                            // resolves the instance from DI on each invocation.
                                            var capturedType = type;
                                            tool = McpServerTool.Create(
                                                method,
                                                (RequestContext<CallToolRequestParams> ctx) =>
                                                    ctx.Services!.GetRequiredService(capturedType),
                                                options);
                                        }

                                        entries.Add(new ToolEntry(tool, minimumRole));
                                    }
                                }

                                return entries;
                            }

                            private sealed record ToolEntry(McpServerTool Tool, UserRole MinimumRole);
                        }

                    
                
Przykład reprezentowania użytkowników
za pomocą tokenów

Demo TokenUserResolver

                    






                        /// <summary>
                        /// Simple token => user resolver for demo/presentation purposes.
                        /// In production this would validate JWT / OAuth tokens.
                        /// </summary>
                        public sealed class TokenUserResolver
                        {
                            // Hard-coded demo tokens. Easy to show on a presentation slide
                            private static readonly Dictionary<string, UserContext> Users 
                                = new(StringComparer.OrdinalIgnoreCase)
                            {
                                ["manager-token"] = new UserContext
                                {
                                    UserId = "user-1",
                                    Name = "Anna Kowalska",
                                    Role = UserRole.Manager
                                },
                                ["employee-token"] = new UserContext
                                {
                                    UserId = "user-2",
                                    Name = "Jan Nowak",
                                    Role = UserRole.Employee
                                }
                            };

                            public UserContext? Resolve(string? bearerToken) =>
                                bearerToken is not null && Users.TryGetValue(bearerToken, out var user)
                                    ? user
                                    : null;
                        }

                    
                
Serce rozwiązania
leży w "program.cs"

Program.cs

                    




























































































































































                        using CompanyDocuments.BaseMCP;
                        using CompanyDocuments.Common.Database;
                        using CompanyDocuments.Common.Services;
                        using CompanyDocuments.MCPWeb;
                        using Microsoft.Extensions.Options;
                        using ModelContextProtocol.Protocol;

                        var builder = WebApplication.CreateBuilder(args);

                        // HttpContextAccessor — needed so MCP tools can read Bearer token
                        builder.Services.AddHttpContextAccessor();

                        // Auth: token → UserContext resolver
                        builder.Services.AddSingleton<TokenUserResolver>();

                        // Pseudo services for demo
                        builder.Services.AddSingleton<SalesService>();
                        builder.Services.AddSingleton<EmployeeService>();
                        builder.Services.AddSingleton<FinancialService>();
                        builder.Services.AddSingleton<TaskService>();
                        builder.Services.AddSingleton<ReportService>();

                        // Tool class DI registrations (needed for instance-method tool creation)
                        builder.Services.AddScoped<ManagerTools>();
                        builder.Services.AddScoped<EmployeeTools>();
                        builder.Services.AddScoped<ReportTools>();
                        builder.Services.AddScoped<ElicitationTools>();
                        builder.Services.AddScoped<SamplingTools>();

                        // Database
                        var defaultDbPath = Path.Combine(
                            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
                            "MCPServerDocuments",
                            "documents.db");

                        builder.Services.AddSingleton(new DatabaseConfig
                        {
                            ConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING") ?? defaultDbPath
                        });

                        builder.Services.AddSingleton<CompanyDocumentsRepository>();
                        builder.Services.AddSingleton<UserHistoryRepository>();
                        builder.Services.AddSingleton<ExcelService>();

                        builder.Services.Configure<MailgunConfig>(builder.Configuration.GetSection("Mailgun"));
                        builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<MailgunConfig>>().Value);
                        builder.Services.AddSingleton<MailgunEmailService>();

                        // RoleBasedToolProvider — discovers tools from assembly, filters by role
                        builder.Services.AddSingleton<RoleBasedToolProvider>();

                        //  MCP Server — NO .WithTools<T>() !!
                        //  We use custom handlers to control what tools/list and tools/call return.
                        //  Issue csharp-sdk#707: WithTools always appends to ListToolsHandler result.

                        builder.Services
                            .AddMcpServer()
                            .WithHttpTransport()
                            .WithPromptsFromAssembly()
                            .WithResourcesFromAssembly()
                            .WithListToolsHandler((context, cancellationToken) =>
                            {
                                var accessor = context.Services!.GetRequiredService<IHttpContextAccessor>();
                                var user = accessor.HttpContext?.Items["UserContext"] as UserContext;
                                var provider = context.Services!.GetRequiredService<RoleBasedToolProvider>();

                                var tools = provider.GetToolsForUser(user);

                                var result = new ListToolsResult
                                {
                                    Tools = tools.Select(t => t.ProtocolTool).ToList()
                                };

                                return ValueTask.FromResult(result);
                            })
                            .WithCallToolHandler(async (context, cancellationToken) =>
                            {
                                var accessor = context.Services!.GetRequiredService<IHttpContextAccessor>();
                                var user = accessor.HttpContext?.Items["UserContext"] as UserContext;

                                if (user is null)
                                {
                                    return new CallToolResult
                                    {
                                        IsError = true,
                                        Content = [new TextContentBlock
                                        {
                                            Text = "Brak autoryzacji. Wyślij nagłówek: Authorization: Bearer <token>"
                                        }]
                                    };
                                }

                                var provider = context.Services!.GetRequiredService<RoleBasedToolProvider>();
                                var toolName = context.Params?.Name ?? string.Empty;
                                var tool = provider.FindTool(toolName, user);

                                if (tool is null)
                                {
                                    return new CallToolResult
                                    {
                                        IsError = true,
                                        Content = [new TextContentBlock
                                        {
                                            Text = $"Narzędzie '{toolName}' nie istnieje lub nie masz do niego dostępu."
                                        }]
                                    };
                                }

                                return await tool.InvokeAsync(context, cancellationToken);
                            });

                        var app = builder.Build();

                        // Initialize database
                        var dbConfig = app.Services.GetRequiredService<DatabaseConfig>();
                        await DatabaseInitializer.InitializeDatabaseAsync(dbConfig.ConnectionString);

                        // Middleware: resolve user from Bearer token → HttpContext.Items
                        app.Use(async (context, next) =>
                        {
                            if (context.Request.Path.StartsWithSegments("/mcp"))
                            {
                                var authHeader = context.Request.Headers.Authorization.FirstOrDefault();
                                if (authHeader?.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase) is true)
                                {
                                    var token = authHeader["Bearer ".Length..];
                                    var resolver = context.RequestServices.GetRequiredService<TokenUserResolver>();
                                    var user = resolver.Resolve(token);

                                    if (user is not null)
                                    {
                                        context.Items["UserContext"] = user;
                                    }
                                }
                            }

                            await next();
                        });

                        // Map MCP endpoints
                        app.MapMcp("/mcp");

                        // Landing page with all endpoints
                        app.MapGet("/", (HttpContext ctx) =>
                        {
                            var baseUrl = $"{ctx.Request.Scheme}://{ctx.Request.Host}";
                            return Results.Content(LandingPage.Render(baseUrl), "text/html");
                        });

                        // ── Health check — shows demo tokens and endpoints
                        app.MapGet("/health", () => Results.Ok(new
                        {
                            status = "healthy",
                            timestamp = DateTime.UtcNow,
                            endpoints = new
                            {
                                http = "http://localhost:3001/mcp",
                                https = "https://localhost:3002/mcp"
                            },
                            demo_tokens = new
                            {
                                manager = "Bearer manager-token => Anna Kowalska (Manager) => 7 tools",
                                employee = "Bearer employee-token => Jan Nowak (Employee) => 4 tools",
                                no_token = "No header => 0 tools"
                            }
                        }));

                        // Diagnostic: who am I?
                        app.MapGet("/whoami", (HttpContext ctx) =>
                        {
                            var user = ctx.Items["UserContext"] as UserContext;
                            return user is not null
                                ? Results.Ok(new { user.UserId, user.Name, Role = user.Role.ToString() })
                                : Results.Unauthorized();
                        });

                        app.Run();

                    
                
Czy to działa?
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
Jak to podłączyć?
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW
Autoryzacja z ścieżką
OAuth 2.0
blog
CW
blog
CW
blog
CW
blog
CW
blog
CW

Program.cs

                    













































































































































































































































                        using CompanyDocuments.BaseMCP;
                        using CompanyDocuments.Common.Database;
                        using CompanyDocuments.Common.Services;
                        using CompanyDocuments.MCPWebAuth2.Auth;
                        using Microsoft.AspNetCore.Authentication.JwtBearer;
                        using Microsoft.Extensions.Options;
                        using Microsoft.IdentityModel.Tokens;
                        using ModelContextProtocol.Protocol;
                        using System.Security.Claims;

                        var builder = WebApplication.CreateBuilder(args);

                        //  1. Authentication: JWT Bearer (tokens issued by our demo OAuth server)

                        var serverUrl = builder.Configuration["ServerUrl"] ?? "http://localhost:3001";

                        // Important it didnt work without CORS
                        // the browser would block the response from /token endpoint and the client would never receive the access token
                        builder.Services.AddCors(options =>
                        {
                            options.AddDefaultPolicy(policy =>
                            {
                                policy.AllowAnyOrigin()
                                    .AllowAnyHeader()
                                    .AllowAnyMethod();
                            });
                        });


                        builder.Services
                            .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                            .AddJwtBearer(options =>
                            {
                                options.TokenValidationParameters = new TokenValidationParameters
                                {
                                    ValidateIssuer = true,
                                    ValidIssuer = DemoOAuthServer.Issuer,
                                    ValidateAudience = true,
                                    ValidAudience = DemoOAuthServer.Audience,
                                    ValidateLifetime = true,
                                    ValidateIssuerSigningKey = true,
                                    IssuerSigningKey = DemoOAuthServer.SigningKey,
                                };
                            })
                            // MCP Auth scheme: serves /.well-known/oauth-protected-resource
                            // and adds resource_metadata URI to WWW-Authenticate challenges
                            .AddMcp(options =>
                            {
                                options.ResourceMetadata = new()
                                {
                                    Resource = new Uri($"{serverUrl}/mcp"),
                                    AuthorizationServers = { new Uri(serverUrl) },
                                    ScopesSupported = { "mcp:tools", "mcp:resources" },
                                };
                            });

                        builder.Services.AddAuthorization();
                        builder.Services.AddHttpContextAccessor();

                        //  2. Business services (reused from your existing projects)

                        builder.Services.AddSingleton<TokenUserResolver>();
                        builder.Services.AddSingleton<SalesService>();
                        builder.Services.AddSingleton<EmployeeService>();
                        builder.Services.AddSingleton<FinancialService>();
                        builder.Services.AddSingleton<TaskService>();
                        builder.Services.AddSingleton<ReportService>();

                        builder.Services.AddScoped<ManagerTools>();
                        builder.Services.AddScoped<EmployeeTools>();
                        builder.Services.AddScoped<ReportTools>();
                        builder.Services.AddScoped<ElicitationTools>();
                        builder.Services.AddScoped<SamplingTools>();

                        // Database
                        var defaultDbPath = Path.Combine(
                            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
                            "MCPServerDocuments", "documents.db");

                        builder.Services.AddSingleton(new DatabaseConfig
                        {
                            ConnectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING") ?? defaultDbPath
                        });

                        builder.Services.AddSingleton<CompanyDocumentsRepository>();
                        builder.Services.AddSingleton<UserHistoryRepository>();
                        builder.Services.AddSingleton<ExcelService>();

                        builder.Services.Configure<MailgunConfig>(builder.Configuration.GetSection("Mailgun"));
                        builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<MailgunConfig>>().Value);
                        builder.Services.AddSingleton<MailgunEmailService>();

                        // Role-based tool provider
                        builder.Services.AddSingleton<RoleBasedToolProvider>();

                        //  3. MCP Server — with HTTP transport + role-based tool filtering
                        //     Using custom handlers (same pattern as MCPWeb project)

                        builder.Services
                            .AddMcpServer()
                            .WithHttpTransport()
                            .WithPromptsFromAssembly()
                            .WithResourcesFromAssembly()
                            .WithListToolsHandler((context, cancellationToken) =>
                            {
                                var user = ResolveUserFromClaims(context.Services!);
                                var provider = context.Services!.GetRequiredService<RoleBasedToolProvider>();
                                var tools = provider.GetToolsForUser(user);

                                return ValueTask.FromResult(new ListToolsResult
                                {
                                    Tools = tools.Select(t => t.ProtocolTool).ToList()
                                });
                            })
                            .WithCallToolHandler(async (context, cancellationToken) =>
                            {
                                var user = ResolveUserFromClaims(context.Services!);

                                if (user is null)
                                {
                                    return new CallToolResult
                                    {
                                        IsError = true,
                                        Content = [new TextContentBlock
                                        {
                                            Text = "Brak autoryzacji. Zaloguj się przez OAuth 2.0 flow."
                                        }]
                                    };
                                }

                                var provider = context.Services!.GetRequiredService<RoleBasedToolProvider>();
                                var toolName = context.Params?.Name ?? string.Empty;
                                var tool = provider.FindTool(toolName, user);

                                if (tool is null)
                                {
                                    return new CallToolResult
                                    {
                                        IsError = true,
                                        Content = [new TextContentBlock
                                        {
                                            Text = $"Narzędzie '{toolName}' nie istnieje lub nie masz do niego dostępu."
                                        }]
                                    };
                                }

                                return await tool.InvokeAsync(context, cancellationToken);
                            });

                        var app = builder.Build();

                        //  4. Initialize database

                        var dbConfig = app.Services.GetRequiredService<DatabaseConfig>();
                        await DatabaseInitializer.InitializeDatabaseAsync(dbConfig.ConnectionString);

                        //  5. Middleware pipeline

                        app.UseCors();
                        app.UseAuthentication();
                        app.UseAuthorization();

                        // Resolve UserContext from JWT claims (for MCP tool handlers)
                        app.Use(async (context, next) =>
                        {
                            if (context.Request.Path.StartsWithSegments("/mcp") && context.User.Identity?.IsAuthenticated == true)
                            {
                                var role = context.User.FindFirst(ClaimTypes.Role)?.Value ?? "Employee";
                                var name = context.User.FindFirst(ClaimTypes.Name)?.Value
                                        ?? context.User.FindFirst("name")?.Value
                                        ?? "Unknown";
                                var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
                                        ?? context.User.FindFirst("sub")?.Value
                                        ?? "0";

                                context.Items["UserContext"] = new UserContext
                                {
                                    UserId = userId,
                                    Name = name,
                                    Role = Enum.TryParse<UserRole>(role, true, out var r) ? r : UserRole.Employee
                                };
                            }
                            await next();
                        });

                        //  6. Map endpoints

                        // OAuth 2.0 Authorization Server endpoints
                        DemoOAuthServer.MapOAuthEndpoints(app, serverUrl);

                        // MCP endpoint (protected by JWT auth)
                        app.MapMcp("/mcp");

                        // Landing page
                        app.MapGet("/", (HttpContext ctx) =>
                        {
                            var baseUrl = $"{ctx.Request.Scheme}://{ctx.Request.Host}";
                            return Results.Content(OAuth2LandingPage.Render(baseUrl), "text/html");
                        });

                        // Health check
                        app.MapGet("/health", () => Results.Ok(new
                        {
                            status = "healthy",
                            timestamp = DateTime.UtcNow,
                            auth = "OAuth 2.0 Authorization Code + PKCE",
                            endpoints = new
                            {
                                mcp = $"{serverUrl}/mcp",
                                authorize = $"{serverUrl}/authorize",
                                token = $"{serverUrl}/token",
                                metadata = $"{serverUrl}/.well-known/oauth-authorization-server",
                                resource_metadata = $"{serverUrl}/.well-known/oauth-protected-resource",
                            },
                            demo_users = new
                            {
                                manager = "anna / pass → Manager role → 7 tools",
                                employee = "jan / pass → Employee role → 4 tools",
                            }
                        }));

                        // Who am I (from JWT)
                        app.MapGet("/whoami", (HttpContext ctx) =>
                        {
                            var user = ctx.Items["UserContext"] as UserContext;
                            if (user is not null)
                                return Results.Ok(new { user.UserId, user.Name, Role = user.Role.ToString() });

                            if (ctx.User.Identity?.IsAuthenticated == true)
                                return Results.Ok(new
                                {
                                    sub = ctx.User.FindFirst("sub")?.Value,
                                    name = ctx.User.FindFirst("name")?.Value,
                                    role = ctx.User.FindFirst(ClaimTypes.Role)?.Value,
                                });

                            return Results.Unauthorized();
                        }).RequireAuthorization();

                        app.Run();


                        static UserContext? ResolveUserFromClaims(IServiceProvider services)
                        {
                            var accessor = services.GetRequiredService<IHttpContextAccessor>();
                            var httpContext = accessor.HttpContext;

                            if (httpContext?.Items["UserContext"] is UserContext uc)
                                return uc;

                            var principal = httpContext?.User;
                            if (principal?.Identity?.IsAuthenticated != true)
                                return null;

                            var role = principal.FindFirst(ClaimTypes.Role)?.Value ?? "Employee";
                            var name = principal.FindFirst(ClaimTypes.Name)?.Value
                                    ?? principal.FindFirst("name")?.Value
                                    ?? "Unknown";
                            var userId = principal.FindFirst(ClaimTypes.NameIdentifier)?.Value
                                    ?? principal.FindFirst("sub")?.Value
                                    ?? "0";

                            return new UserContext
                            {
                                UserId = userId,
                                Name = name,
                                Role = Enum.TryParse<UserRole>(role, true, out var r) ? r : UserRole.Employee
                            };
                        }
                                
                    
                
Czy to działa dla aplikacji?
blog
CW
Podsumowanie
  1. MCP ma moc
blog
CW